From 7f49df1503080b909acebc38bbb3e8acee1af9b4 Mon Sep 17 00:00:00 2001
From: "shakespeare.diy"
Date: Mon, 7 Sep 2026 20:20:51 +0700
Subject: [PATCH] Wallet UX polish: elegant recovery-phrase reveal, separate
amounts, loading states
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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
---
package.json | 2 +-
src/components/wallet/CashuWalletCard.tsx | 67 ++++++---
.../wallet/RecoveryPhraseDialog.tsx | 141 ++++++++++++++++++
src/hooks/wallet/useLightningOperations.ts | 13 +-
src/hooks/wallet/useTokenOperations.ts | 24 ++-
5 files changed, 215 insertions(+), 32 deletions(-)
create mode 100644 src/components/wallet/RecoveryPhraseDialog.tsx
diff --git a/package.json b/package.json
index fb353cd..a133b29 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "skate-spots",
"private": true,
- "version": "1.2.21",
+ "version": "1.2.22",
"type": "module",
"homepage": "https://skate-spots.com",
"scripts": {
diff --git a/src/components/wallet/CashuWalletCard.tsx b/src/components/wallet/CashuWalletCard.tsx
index 98e8b1e..934cd25 100644
--- a/src/components/wallet/CashuWalletCard.tsx
+++ b/src/components/wallet/CashuWalletCard.tsx
@@ -39,6 +39,7 @@ import {
import { useToast } from '@/hooks/useToast';
import { QRScannerModal } from '@/components/wallet/QRScannerModal';
import { MintPrivacyManager } from '@/components/wallet/MintPrivacyManager';
+import { RecoveryPhraseDialog } from '@/components/wallet/RecoveryPhraseDialog';
import { isNativePlatform } from '@/lib/platform';
import { Smartphone } from 'lucide-react';
@@ -168,6 +169,7 @@ function CashuWalletCardNative() {
setShowInvoiceModal,
currentInvoice,
handleLightningReceive,
+ isGeneratingInvoice,
lightningInvoice,
setLightningInvoice,
isProcessingPayment,
@@ -178,9 +180,11 @@ function CashuWalletCardNative() {
tokenInput,
setTokenInput,
handleTokenReceive,
+ isReceivingToken,
sendAmount,
setSendAmount,
handleTokenSend,
+ isCreatingToken,
sentToken,
setSentToken,
sentTokenQRCode,
@@ -194,9 +198,15 @@ function CashuWalletCardNative() {
// QR Scanner state
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 [lightningAddressAmount, setLightningAddressAmount] = useState('');
// Set first mint as active if none selected
useEffect(() => {
@@ -242,34 +252,39 @@ function CashuWalletCardNative() {
}, [coco, setTokenInput, setLightningInvoice, toast]);
// Handle paying a Lightning address
+ const [isPayingAddress, setIsPayingAddress] = useState(false);
const handlePayLightningAddress = useCallback(async () => {
- if (!lightningAddress || !sendAmount || !coco || !activeMintUrl) return;
-
+ if (!lightningAddress || !lightningAddressAmount || !coco || !activeMintUrl) return;
+
try {
- const amount = parseInt(sendAmount);
+ const amount = parseInt(lightningAddressAmount);
if (isNaN(amount) || amount <= 0) {
toast({ variant: 'destructive', title: 'Invalid amount', description: 'Please enter a valid amount' });
return;
}
+ setIsPayingAddress(true);
+
// First resolve the Lightning address to get an invoice
const invoice = await getLightningInvoice({ lightningAddress, amountSats: amount });
-
+
// Then pay the invoice using Cashu
const meltQuote = await coco.quotes.createMeltQuote(activeMintUrl, invoice);
await coco.quotes.payMeltQuote(activeMintUrl, meltQuote.quote);
-
+
toast({ title: 'Payment sent', description: `Sent ${amount} sats to ${lightningAddress}` });
setLightningAddress('');
- setSendAmount('');
+ setLightningAddressAmount('');
} catch (err) {
toast({
variant: 'destructive',
title: 'Payment failed',
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
const copyToClipboard = async (text: string, label: string) => {
@@ -406,10 +421,10 @@ function CashuWalletCardNative() {
/>
@@ -446,10 +461,10 @@ function CashuWalletCardNative() {
/>
@@ -471,10 +486,10 @@ function CashuWalletCardNative() {
/>
@@ -543,16 +558,16 @@ function CashuWalletCardNative() {
type="number"
inputMode="numeric"
placeholder="Amount in sats"
- value={sendAmount}
- onChange={(e) => setSendAmount(e.target.value)}
+ value={lightningAddressAmount}
+ onChange={(e) => setLightningAddressAmount(e.target.value)}
className="h-11"
/>
@@ -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.