mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-13 08:57:04 +02:00
feat: full-width custom amount and wallet timeout with QR fallback
Custom Amount Input:
- Change custom amount input from w-24 to flex-1
- Now takes full remaining width in flex row
- Better UX on smaller screens and more obvious input field
Wallet Payment Timeout Handling:
- Add 30 second timeout to wallet payments using Promise.race
- On timeout, automatically show QR code as fallback
- Add paymentTimedOut state to track timeout condition
- Toast warning when payment times out
Retry with Wallet Feature:
- Add handleRetryWallet function to retry timed out payment
- Show "Retry with Wallet" button in QR dialog when timed out
- Button only appears if wallet is connected and payment capable
- Retry uses same 30s timeout, shows error if fails again
- Provides loading state with spinner during retry
User Flow:
1. User attempts wallet payment
2. If timeout after 30s, shows QR code automatically
3. User can scan QR to pay manually OR
4. User can click "Retry with Wallet" to try again
5. If retry times out, stays on QR for manual payment
Implementation Details:
- Promise.race between payInvoice and 30s timeout
- Timeout throws Error("TIMEOUT") for easy detection
- QR dialog conditionally shows retry button
- Retry resets state and attempts payment again
- Console logging for debugging timeout issues
All tests passing (939 passed)
Build successful
This commit is contained in:
@@ -125,6 +125,7 @@ export function ZapWindow({
|
||||
const [invoice, setInvoice] = useState<string>("");
|
||||
const [showQrDialog, setShowQrDialog] = useState(false);
|
||||
const [showLogin, setShowLogin] = useState(false);
|
||||
const [paymentTimedOut, setPaymentTimedOut] = useState(false);
|
||||
|
||||
// Editor ref and search functions
|
||||
const editorRef = useRef<MentionEditorHandle>(null);
|
||||
@@ -334,25 +335,48 @@ export function ZapWindow({
|
||||
|
||||
// Step 5: Pay or show QR code
|
||||
if (useWallet && wallet && walletInfo?.methods.includes("pay_invoice")) {
|
||||
// Pay with NWC wallet
|
||||
// Pay with NWC wallet with timeout
|
||||
toast.info("Paying invoice with wallet...");
|
||||
await payInvoice(invoiceText);
|
||||
await refreshBalance();
|
||||
|
||||
setIsPaid(true);
|
||||
toast.success(
|
||||
`⚡ Zapped ${amount} sats to ${recipientProfile?.name || recipientName}!`,
|
||||
);
|
||||
try {
|
||||
// Race between payment and 30 second timeout
|
||||
const paymentPromise = payInvoice(invoiceText);
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("TIMEOUT")), 30000),
|
||||
);
|
||||
|
||||
// Show success message from LNURL service if available
|
||||
if (invoiceResponse.successAction?.message) {
|
||||
toast.info(invoiceResponse.successAction.message);
|
||||
}
|
||||
await Promise.race([paymentPromise, timeoutPromise]);
|
||||
await refreshBalance();
|
||||
|
||||
// Close the window after successful zap
|
||||
if (onClose) {
|
||||
// Small delay to let the user see the success toast
|
||||
setTimeout(() => onClose(), 1500);
|
||||
setIsPaid(true);
|
||||
toast.success(
|
||||
`⚡ Zapped ${amount} sats to ${recipientProfile?.name || recipientName}!`,
|
||||
);
|
||||
|
||||
// Show success message from LNURL service if available
|
||||
if (invoiceResponse.successAction?.message) {
|
||||
toast.info(invoiceResponse.successAction.message);
|
||||
}
|
||||
|
||||
// Close the window after successful zap
|
||||
if (onClose) {
|
||||
// Small delay to let the user see the success toast
|
||||
setTimeout(() => onClose(), 1500);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === "TIMEOUT") {
|
||||
// Payment timed out - show QR code with retry option
|
||||
console.log("[Zap] Wallet payment timed out, showing QR code");
|
||||
toast.warning("Wallet payment timed out. Showing QR code instead.");
|
||||
setPaymentTimedOut(true);
|
||||
const qrUrl = await generateQrCode(invoiceText);
|
||||
setQrCodeUrl(qrUrl);
|
||||
setInvoice(invoiceText);
|
||||
setShowQrDialog(true);
|
||||
} else {
|
||||
// Other payment error - re-throw
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Show QR code and invoice
|
||||
@@ -392,6 +416,51 @@ export function ZapWindow({
|
||||
setShowLogin(true);
|
||||
};
|
||||
|
||||
// Retry wallet payment
|
||||
const handleRetryWallet = async () => {
|
||||
if (!invoice || !wallet) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
setShowQrDialog(false);
|
||||
setPaymentTimedOut(false);
|
||||
|
||||
try {
|
||||
toast.info("Retrying payment with wallet...");
|
||||
|
||||
// Try again with timeout
|
||||
const paymentPromise = payInvoice(invoice);
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("TIMEOUT")), 30000),
|
||||
);
|
||||
|
||||
await Promise.race([paymentPromise, timeoutPromise]);
|
||||
await refreshBalance();
|
||||
|
||||
setIsPaid(true);
|
||||
toast.success("⚡ Payment successful!");
|
||||
|
||||
// Close the window after successful zap
|
||||
if (onClose) {
|
||||
setTimeout(() => onClose(), 1500);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === "TIMEOUT") {
|
||||
console.log("[Zap] Wallet payment timed out again");
|
||||
toast.error("Payment timed out again. Please try manually.");
|
||||
setPaymentTimedOut(true);
|
||||
setShowQrDialog(true);
|
||||
} else {
|
||||
console.error("[Zap] Retry payment error:", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Failed to retry payment",
|
||||
);
|
||||
setShowQrDialog(true);
|
||||
}
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-background overflow-hidden">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
@@ -430,7 +499,7 @@ export function ZapWindow({
|
||||
setSelectedAmount(null);
|
||||
}}
|
||||
min="1"
|
||||
className="w-24 h-9"
|
||||
className="flex-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -547,6 +616,30 @@ export function ZapWindow({
|
||||
Copy Invoice
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Retry with wallet button if payment timed out */}
|
||||
{paymentTimedOut &&
|
||||
wallet &&
|
||||
walletInfo?.methods.includes("pay_invoice") && (
|
||||
<Button
|
||||
onClick={handleRetryWallet}
|
||||
disabled={isProcessing}
|
||||
className="w-full"
|
||||
variant="default"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="size-4 mr-2 animate-spin" />
|
||||
Retrying...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Wallet className="size-4 mr-2" />
|
||||
Retry with Wallet
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user