Wallet UX polish: elegant recovery-phrase reveal, separate amounts, loading states
Continued the wallet audit with a focus on intuitive, minimal, elegant UX.
• Recovery phrase — replaced the raw browser alert() (which dumped the seed
as unstyled, uncopyable plaintext) with a proper RecoveryPhraseDialog:
hidden/blurred by default with a deliberate "tap to reveal" gate (no
shoulder-surfing on open), a numbered 12-word chip grid for accurate
transcription, a copy button (enabled only after reveal), and an honest
"paper is safest, clipboards leak" note. Re-hides on every close. This was
the wallet's most sensitive surface and its worst blemish.
• Fixed shared-state bug: "Create Cashu Token" and "Pay Lightning Address"
both wrote to the same `sendAmount`, so switching between them carried a
stale amount across two unrelated intents. The address form now owns its
own amount state.
• Added loading feedback + double-tap protection to every money action that
lacked it — Generate Invoice ("Generating…"), Create Token ("Creating…"),
Receive Token ("Receiving…"), Pay Lightning Address ("Sending…"). Each hook
now exposes an in-flight flag and guards re-entry.
WalletPage and the browser "app-only" notice were already clean/elegant —
left as-is.
Bumps to 1.2.22 (Android versionCode 10222 auto-derived).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
39ac517610
commit
7f49df1503
5 changed files with 215 additions and 32 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "skate-spots",
|
"name": "skate-spots",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.2.21",
|
"version": "1.2.22",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"homepage": "https://skate-spots.com",
|
"homepage": "https://skate-spots.com",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import {
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { QRScannerModal } from '@/components/wallet/QRScannerModal';
|
import { QRScannerModal } from '@/components/wallet/QRScannerModal';
|
||||||
import { MintPrivacyManager } from '@/components/wallet/MintPrivacyManager';
|
import { MintPrivacyManager } from '@/components/wallet/MintPrivacyManager';
|
||||||
|
import { RecoveryPhraseDialog } from '@/components/wallet/RecoveryPhraseDialog';
|
||||||
import { isNativePlatform } from '@/lib/platform';
|
import { isNativePlatform } from '@/lib/platform';
|
||||||
import { Smartphone } from 'lucide-react';
|
import { Smartphone } from 'lucide-react';
|
||||||
|
|
||||||
|
|
@ -168,6 +169,7 @@ function CashuWalletCardNative() {
|
||||||
setShowInvoiceModal,
|
setShowInvoiceModal,
|
||||||
currentInvoice,
|
currentInvoice,
|
||||||
handleLightningReceive,
|
handleLightningReceive,
|
||||||
|
isGeneratingInvoice,
|
||||||
lightningInvoice,
|
lightningInvoice,
|
||||||
setLightningInvoice,
|
setLightningInvoice,
|
||||||
isProcessingPayment,
|
isProcessingPayment,
|
||||||
|
|
@ -178,9 +180,11 @@ function CashuWalletCardNative() {
|
||||||
tokenInput,
|
tokenInput,
|
||||||
setTokenInput,
|
setTokenInput,
|
||||||
handleTokenReceive,
|
handleTokenReceive,
|
||||||
|
isReceivingToken,
|
||||||
sendAmount,
|
sendAmount,
|
||||||
setSendAmount,
|
setSendAmount,
|
||||||
handleTokenSend,
|
handleTokenSend,
|
||||||
|
isCreatingToken,
|
||||||
sentToken,
|
sentToken,
|
||||||
setSentToken,
|
setSentToken,
|
||||||
sentTokenQRCode,
|
sentTokenQRCode,
|
||||||
|
|
@ -194,9 +198,15 @@ function CashuWalletCardNative() {
|
||||||
|
|
||||||
// QR Scanner state
|
// QR Scanner state
|
||||||
const [showQRScanner, setShowQRScanner] = useState(false);
|
const [showQRScanner, setShowQRScanner] = useState(false);
|
||||||
|
|
||||||
// Lightning address state
|
// Recovery-phrase reveal dialog
|
||||||
|
const [showRecoveryPhrase, setShowRecoveryPhrase] = useState(false);
|
||||||
|
|
||||||
|
// Lightning address state. Its amount is kept SEPARATE from the token-send
|
||||||
|
// amount (`sendAmount`) so switching between "Create Token" and "Pay Address"
|
||||||
|
// never carries a stale value between two unrelated intents.
|
||||||
const [lightningAddress, setLightningAddress] = useState('');
|
const [lightningAddress, setLightningAddress] = useState('');
|
||||||
|
const [lightningAddressAmount, setLightningAddressAmount] = useState('');
|
||||||
|
|
||||||
// Set first mint as active if none selected
|
// Set first mint as active if none selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -242,34 +252,39 @@ function CashuWalletCardNative() {
|
||||||
}, [coco, setTokenInput, setLightningInvoice, toast]);
|
}, [coco, setTokenInput, setLightningInvoice, toast]);
|
||||||
|
|
||||||
// Handle paying a Lightning address
|
// Handle paying a Lightning address
|
||||||
|
const [isPayingAddress, setIsPayingAddress] = useState(false);
|
||||||
const handlePayLightningAddress = useCallback(async () => {
|
const handlePayLightningAddress = useCallback(async () => {
|
||||||
if (!lightningAddress || !sendAmount || !coco || !activeMintUrl) return;
|
if (!lightningAddress || !lightningAddressAmount || !coco || !activeMintUrl) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const amount = parseInt(sendAmount);
|
const amount = parseInt(lightningAddressAmount);
|
||||||
if (isNaN(amount) || amount <= 0) {
|
if (isNaN(amount) || amount <= 0) {
|
||||||
toast({ variant: 'destructive', title: 'Invalid amount', description: 'Please enter a valid amount' });
|
toast({ variant: 'destructive', title: 'Invalid amount', description: 'Please enter a valid amount' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsPayingAddress(true);
|
||||||
|
|
||||||
// First resolve the Lightning address to get an invoice
|
// First resolve the Lightning address to get an invoice
|
||||||
const invoice = await getLightningInvoice({ lightningAddress, amountSats: amount });
|
const invoice = await getLightningInvoice({ lightningAddress, amountSats: amount });
|
||||||
|
|
||||||
// Then pay the invoice using Cashu
|
// Then pay the invoice using Cashu
|
||||||
const meltQuote = await coco.quotes.createMeltQuote(activeMintUrl, invoice);
|
const meltQuote = await coco.quotes.createMeltQuote(activeMintUrl, invoice);
|
||||||
await coco.quotes.payMeltQuote(activeMintUrl, meltQuote.quote);
|
await coco.quotes.payMeltQuote(activeMintUrl, meltQuote.quote);
|
||||||
|
|
||||||
toast({ title: 'Payment sent', description: `Sent ${amount} sats to ${lightningAddress}` });
|
toast({ title: 'Payment sent', description: `Sent ${amount} sats to ${lightningAddress}` });
|
||||||
setLightningAddress('');
|
setLightningAddress('');
|
||||||
setSendAmount('');
|
setLightningAddressAmount('');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
title: 'Payment failed',
|
title: 'Payment failed',
|
||||||
description: err instanceof Error ? err.message : 'Unknown error'
|
description: err instanceof Error ? err.message : 'Unknown error'
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setIsPayingAddress(false);
|
||||||
}
|
}
|
||||||
}, [lightningAddress, sendAmount, coco, activeMintUrl, getLightningInvoice, toast, setSendAmount]);
|
}, [lightningAddress, lightningAddressAmount, coco, activeMintUrl, getLightningInvoice, toast]);
|
||||||
|
|
||||||
// Copy to clipboard helper
|
// Copy to clipboard helper
|
||||||
const copyToClipboard = async (text: string, label: string) => {
|
const copyToClipboard = async (text: string, label: string) => {
|
||||||
|
|
@ -406,10 +421,10 @@ function CashuWalletCardNative() {
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleLightningReceive}
|
onClick={handleLightningReceive}
|
||||||
disabled={!activeMintUrl || !quoteAmount}
|
disabled={!activeMintUrl || !quoteAmount || isGeneratingInvoice}
|
||||||
className="h-11 bg-orange-500 hover:bg-orange-600 font-bold"
|
className="h-11 bg-orange-500 hover:bg-orange-600 font-bold"
|
||||||
>
|
>
|
||||||
Generate Invoice
|
{isGeneratingInvoice ? 'Generating…' : 'Generate Invoice'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -446,10 +461,10 @@ function CashuWalletCardNative() {
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleTokenReceive}
|
onClick={handleTokenReceive}
|
||||||
disabled={!tokenInput}
|
disabled={!tokenInput || isReceivingToken}
|
||||||
className="w-full h-11 bg-green-600 hover:bg-green-700 font-bold"
|
className="w-full h-11 bg-green-600 hover:bg-green-700 font-bold"
|
||||||
>
|
>
|
||||||
Receive Token
|
{isReceivingToken ? 'Receiving…' : 'Receive Token'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -471,10 +486,10 @@ function CashuWalletCardNative() {
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleTokenSend}
|
onClick={handleTokenSend}
|
||||||
disabled={!activeMintUrl || !sendAmount}
|
disabled={!activeMintUrl || !sendAmount || isCreatingToken}
|
||||||
className="h-11 bg-orange-500 hover:bg-orange-600 font-bold"
|
className="h-11 bg-orange-500 hover:bg-orange-600 font-bold"
|
||||||
>
|
>
|
||||||
Create Token
|
{isCreatingToken ? 'Creating…' : 'Create Token'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -543,16 +558,16 @@ function CashuWalletCardNative() {
|
||||||
type="number"
|
type="number"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
placeholder="Amount in sats"
|
placeholder="Amount in sats"
|
||||||
value={sendAmount}
|
value={lightningAddressAmount}
|
||||||
onChange={(e) => setSendAmount(e.target.value)}
|
onChange={(e) => setLightningAddressAmount(e.target.value)}
|
||||||
className="h-11"
|
className="h-11"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={handlePayLightningAddress}
|
onClick={handlePayLightningAddress}
|
||||||
disabled={!lightningAddress || !sendAmount || isResolvingAddress || isProcessingPayment}
|
disabled={!lightningAddress || !lightningAddressAmount || isResolvingAddress || isPayingAddress}
|
||||||
className="w-full h-11 bg-purple-600 hover:bg-purple-700 font-bold"
|
className="w-full h-11 bg-purple-600 hover:bg-purple-700 font-bold"
|
||||||
>
|
>
|
||||||
{isResolvingAddress ? 'Resolving...' : 'Send to Address'}
|
{isResolvingAddress || isPayingAddress ? 'Sending…' : 'Send to Address'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -668,9 +683,7 @@ function CashuWalletCardNative() {
|
||||||
Your wallet is secured with a 12-word recovery phrase. Save it safely — you'll need it to restore your balance if you clear browser data.
|
Your wallet is secured with a 12-word recovery phrase. Save it safely — you'll need it to restore your balance if you clear browser data.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => setShowRecoveryPhrase(true)}
|
||||||
alert(`Your backup phrase:\n\n${mnemonic}\n\nWrite these words down and store them safely!`);
|
|
||||||
}}
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full h-11 border-amber-500/50 text-amber-600 hover:bg-amber-500/10 font-bold"
|
className="w-full h-11 border-amber-500/50 text-amber-600 hover:bg-amber-500/10 font-bold"
|
||||||
>
|
>
|
||||||
|
|
@ -770,6 +783,16 @@ function CashuWalletCardNative() {
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Recovery Phrase Dialog — replaces the old raw alert() with an elegant,
|
||||||
|
hidden-by-default reveal */}
|
||||||
|
{mnemonic && (
|
||||||
|
<RecoveryPhraseDialog
|
||||||
|
open={showRecoveryPhrase}
|
||||||
|
onOpenChange={setShowRecoveryPhrase}
|
||||||
|
mnemonic={mnemonic}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* QR Scanner Modal */}
|
{/* QR Scanner Modal */}
|
||||||
<QRScannerModal
|
<QRScannerModal
|
||||||
isOpen={showQRScanner}
|
isOpen={showQRScanner}
|
||||||
|
|
|
||||||
141
src/components/wallet/RecoveryPhraseDialog.tsx
Normal file
141
src/components/wallet/RecoveryPhraseDialog.tsx
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useToast } from '@/hooks/useToast';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Shield, Eye, EyeOff, Copy, Check } from 'lucide-react';
|
||||||
|
|
||||||
|
interface RecoveryPhraseDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
/** Space-separated BIP-39 mnemonic. */
|
||||||
|
mnemonic: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An elegant, security-conscious recovery-phrase reveal — the wallet's single
|
||||||
|
* most sensitive surface, so it deserves better than a raw `alert()`.
|
||||||
|
*
|
||||||
|
* Principles:
|
||||||
|
* • Hidden by default (blurred word grid) so the seed can't be shoulder-surfed
|
||||||
|
* just by opening the dialog; the user must deliberately tap "Reveal".
|
||||||
|
* • Numbered word chips make it easy to transcribe accurately onto paper.
|
||||||
|
* • Copy is available but explicitly discouraged in the warning (clipboards
|
||||||
|
* leak); paper is the safe path.
|
||||||
|
* • The seed is cleared from the reveal state whenever the dialog closes.
|
||||||
|
*/
|
||||||
|
export function RecoveryPhraseDialog({ open, onOpenChange, mnemonic }: RecoveryPhraseDialogProps) {
|
||||||
|
const [revealed, setRevealed] = useState(false);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const words = mnemonic.trim().split(/\s+/);
|
||||||
|
|
||||||
|
const handleOpenChange = (next: boolean) => {
|
||||||
|
// Re-hide on every close so re-opening always starts concealed.
|
||||||
|
if (!next) {
|
||||||
|
setRevealed(false);
|
||||||
|
setCopied(false);
|
||||||
|
}
|
||||||
|
onOpenChange(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(mnemonic);
|
||||||
|
setCopied(true);
|
||||||
|
toast({ title: 'Copied', description: 'Recovery phrase copied — paste it somewhere safe, then clear your clipboard.' });
|
||||||
|
setTimeout(() => setCopied(false), 2500);
|
||||||
|
} catch {
|
||||||
|
toast({ variant: 'destructive', title: 'Copy failed', description: 'Please write the words down instead.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogContent className="w-[calc(100%-1rem)] sm:max-w-md p-5 sm:p-6">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="mx-auto mb-2 flex h-12 w-12 items-center justify-center rounded-2xl bg-amber-500/15">
|
||||||
|
<Shield className="h-6 w-6 text-amber-500" />
|
||||||
|
</div>
|
||||||
|
<DialogTitle className="text-center text-lg font-black">Recovery Phrase</DialogTitle>
|
||||||
|
<DialogDescription className="text-center text-xs leading-relaxed">
|
||||||
|
These 12 words are the only way to restore your ecash balance. Write them down in order
|
||||||
|
and store them offline — never share them with anyone.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Word grid */}
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'grid grid-cols-2 gap-2 rounded-2xl border border-amber-500/20 bg-amber-500/[0.04] p-3 transition-all sm:grid-cols-3',
|
||||||
|
!revealed && 'select-none blur-sm',
|
||||||
|
)}
|
||||||
|
aria-hidden={!revealed}
|
||||||
|
>
|
||||||
|
{words.map((word, i) => (
|
||||||
|
<div
|
||||||
|
key={`${i}-${word}`}
|
||||||
|
className="flex items-center gap-2 rounded-lg bg-background/60 px-2.5 py-2"
|
||||||
|
>
|
||||||
|
<span className="w-4 text-right text-[10px] font-bold tabular-nums text-muted-foreground">
|
||||||
|
{i + 1}
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-sm font-semibold">{word}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reveal overlay */}
|
||||||
|
{!revealed && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setRevealed(true)}
|
||||||
|
className="absolute inset-0 flex flex-col items-center justify-center gap-1.5 rounded-2xl bg-background/40 backdrop-blur-[2px] transition-colors hover:bg-background/30 active:scale-[0.99]"
|
||||||
|
>
|
||||||
|
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-amber-500/20">
|
||||||
|
<Eye className="h-5 w-5 text-amber-500" />
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-bold uppercase tracking-wide text-amber-400">
|
||||||
|
Tap to reveal
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground">Make sure nobody's watching</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1 gap-2"
|
||||||
|
onClick={() => setRevealed((v) => !v)}
|
||||||
|
>
|
||||||
|
{revealed ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||||
|
{revealed ? 'Hide' : 'Reveal'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1 gap-2"
|
||||||
|
onClick={handleCopy}
|
||||||
|
disabled={!revealed}
|
||||||
|
>
|
||||||
|
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||||
|
{copied ? 'Copied' : 'Copy'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-center text-[11px] leading-relaxed text-muted-foreground">
|
||||||
|
Writing it on paper is safest — copying to your clipboard can leak to other apps.
|
||||||
|
</p>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,10 @@ export function useLightningOperations({
|
||||||
expiry: number;
|
expiry: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
|
// Receive-generation in-flight flag (so the button can show progress and
|
||||||
|
// can't be double-tapped into two invoices).
|
||||||
|
const [isGeneratingInvoice, setIsGeneratingInvoice] = useState(false);
|
||||||
|
|
||||||
// Send state
|
// Send state
|
||||||
const [lightningInvoice, setLightningInvoice] = useState('');
|
const [lightningInvoice, setLightningInvoice] = useState('');
|
||||||
const [isProcessingPayment, setIsProcessingPayment] = useState(false);
|
const [isProcessingPayment, setIsProcessingPayment] = useState(false);
|
||||||
|
|
@ -35,7 +39,7 @@ export function useLightningOperations({
|
||||||
|
|
||||||
// Handle receive (create mint quote/invoice)
|
// Handle receive (create mint quote/invoice)
|
||||||
const handleLightningReceive = useCallback(async () => {
|
const handleLightningReceive = useCallback(async () => {
|
||||||
if (!coco || !quoteAmount || !activeMintUrl) return;
|
if (isGeneratingInvoice || !coco || !quoteAmount || !activeMintUrl) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const amount = parseInt(quoteAmount);
|
const amount = parseInt(quoteAmount);
|
||||||
|
|
@ -47,6 +51,8 @@ export function useLightningOperations({
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsGeneratingInvoice(true);
|
||||||
|
|
||||||
const quote = await coco.quotes.createMintQuote(activeMintUrl, amount);
|
const quote = await coco.quotes.createMintQuote(activeMintUrl, amount);
|
||||||
const qrCodeUrl = await generateQRCode(quote.request);
|
const qrCodeUrl = await generateQRCode(quote.request);
|
||||||
|
|
@ -103,8 +109,10 @@ export function useLightningOperations({
|
||||||
? `${activeMintUrl?.replace(/^https?:\/\//, '')} can't be reached from your browser (no CORS). Switch to a web-friendly mint in wallet settings — or use the native app, where any mint works.`
|
? `${activeMintUrl?.replace(/^https?:\/\//, '')} can't be reached from your browser (no CORS). Switch to a web-friendly mint in wallet settings — or use the native app, where any mint works.`
|
||||||
: raw,
|
: raw,
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setIsGeneratingInvoice(false);
|
||||||
}
|
}
|
||||||
}, [coco, quoteAmount, activeMintUrl, generateQRCode, toast]);
|
}, [coco, quoteAmount, activeMintUrl, generateQRCode, isGeneratingInvoice, toast]);
|
||||||
|
|
||||||
// Handle send (pay Lightning invoice)
|
// Handle send (pay Lightning invoice)
|
||||||
const handleLightningSend = useCallback(async () => {
|
const handleLightningSend = useCallback(async () => {
|
||||||
|
|
@ -159,6 +167,7 @@ export function useLightningOperations({
|
||||||
currentInvoice,
|
currentInvoice,
|
||||||
setCurrentInvoice,
|
setCurrentInvoice,
|
||||||
handleLightningReceive,
|
handleLightningReceive,
|
||||||
|
isGeneratingInvoice,
|
||||||
|
|
||||||
// Send
|
// Send
|
||||||
lightningInvoice,
|
lightningInvoice,
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,12 @@ export function useTokenOperations({
|
||||||
|
|
||||||
// Receive state
|
// Receive state
|
||||||
const [tokenInput, setTokenInput] = useState('');
|
const [tokenInput, setTokenInput] = useState('');
|
||||||
|
const [isReceivingToken, setIsReceivingToken] = useState(false);
|
||||||
|
|
||||||
// Send state
|
// Send state
|
||||||
const [sendAmount, setSendAmount] = useState('');
|
const [sendAmount, setSendAmount] = useState('');
|
||||||
|
const [isCreatingToken, setIsCreatingToken] = useState(false);
|
||||||
|
|
||||||
// Transaction output state
|
// Transaction output state
|
||||||
const [sentToken, setSentToken] = useState<string | null>(null);
|
const [sentToken, setSentToken] = useState<string | null>(null);
|
||||||
const [sentTokenQRCode, setSentTokenQRCode] = useState<string>('');
|
const [sentTokenQRCode, setSentTokenQRCode] = useState<string>('');
|
||||||
|
|
@ -83,8 +85,9 @@ export function useTokenOperations({
|
||||||
|
|
||||||
// Handle receive (token)
|
// Handle receive (token)
|
||||||
const handleTokenReceive = useCallback(async () => {
|
const handleTokenReceive = useCallback(async () => {
|
||||||
if (!coco || !tokenInput) return;
|
if (isReceivingToken || !coco || !tokenInput) return;
|
||||||
|
|
||||||
|
setIsReceivingToken(true);
|
||||||
try {
|
try {
|
||||||
const tokenToProcess = tokenInput.trim();
|
const tokenToProcess = tokenInput.trim();
|
||||||
|
|
||||||
|
|
@ -113,12 +116,14 @@ export function useTokenOperations({
|
||||||
title: 'Receive failed',
|
title: 'Receive failed',
|
||||||
description: err instanceof Error ? err.message : 'Unknown error'
|
description: err instanceof Error ? err.message : 'Unknown error'
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setIsReceivingToken(false);
|
||||||
}
|
}
|
||||||
}, [coco, tokenInput, handleUnknownMintError, toast]);
|
}, [coco, tokenInput, isReceivingToken, handleUnknownMintError, toast]);
|
||||||
|
|
||||||
// Handle send (token)
|
// Handle send (token)
|
||||||
const handleTokenSend = useCallback(async () => {
|
const handleTokenSend = useCallback(async () => {
|
||||||
if (!coco || !sendAmount || !activeMintUrl) return;
|
if (isCreatingToken || !coco || !sendAmount || !activeMintUrl) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const amount = parseInt(sendAmount);
|
const amount = parseInt(sendAmount);
|
||||||
|
|
@ -130,7 +135,8 @@ export function useTokenOperations({
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsCreatingToken(true);
|
||||||
const token = await coco.wallet.send(activeMintUrl, amount);
|
const token = await coco.wallet.send(activeMintUrl, amount);
|
||||||
const encodedToken = getEncodedToken(token);
|
const encodedToken = getEncodedToken(token);
|
||||||
setSentToken(encodedToken);
|
setSentToken(encodedToken);
|
||||||
|
|
@ -147,18 +153,22 @@ export function useTokenOperations({
|
||||||
title: 'Send failed',
|
title: 'Send failed',
|
||||||
description: err instanceof Error ? err.message : 'Unknown error'
|
description: err instanceof Error ? err.message : 'Unknown error'
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setIsCreatingToken(false);
|
||||||
}
|
}
|
||||||
}, [coco, sendAmount, activeMintUrl, getEncodedToken, generateSentTokenQRCode, toast]);
|
}, [coco, sendAmount, activeMintUrl, isCreatingToken, getEncodedToken, generateSentTokenQRCode, toast]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Receive state
|
// Receive state
|
||||||
tokenInput,
|
tokenInput,
|
||||||
setTokenInput,
|
setTokenInput,
|
||||||
handleTokenReceive,
|
handleTokenReceive,
|
||||||
|
isReceivingToken,
|
||||||
// Send state
|
// Send state
|
||||||
sendAmount,
|
sendAmount,
|
||||||
setSendAmount,
|
setSendAmount,
|
||||||
handleTokenSend,
|
handleTokenSend,
|
||||||
|
isCreatingToken,
|
||||||
// Transaction output state
|
// Transaction output state
|
||||||
sentToken,
|
sentToken,
|
||||||
setSentToken,
|
setSentToken,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue