From a0df11c522bd5151ff158d686205bbdea261ce9a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Jan 2026 22:13:26 +0000 Subject: [PATCH] feat: full-width custom amount and wallet timeout with QR fallback QR code improvements: - Add profile picture overlay in center of QR code (25% size, circular) - Remove redundant "Copy Invoice" button (keep icon button only) - Show "Open in Wallet" as full-width button UI improvements: - Use UserName component everywhere (clickable, styled, shows Grimoire members) - Custom amount now full-width on separate line - Better visual hierarchy Default amounts updated: - Changed from [21, 100, 500, 1000, 5000, 10000] - To [21, 420, 2100, 42000] - More aligned with common zap amounts The profile picture overlay helps users identify who they're zapping while maintaining QR code scannability. UserName component provides consistent styling and clickable profile links. --- src/components/ZapWindow.tsx | 102 +++++++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 23 deletions(-) diff --git a/src/components/ZapWindow.tsx b/src/components/ZapWindow.tsx index bd89f21..861327e 100644 --- a/src/components/ZapWindow.tsx +++ b/src/components/ZapWindow.tsx @@ -32,6 +32,7 @@ import eventStore from "@/services/event-store"; import { useWallet } from "@/hooks/useWallet"; import { getDisplayName } from "@/lib/nostr-utils"; import { KindRenderer } from "./nostr/kinds"; +import { UserName } from "./nostr/UserName"; import type { EventPointer, AddressPointer } from "@/lib/open-parser"; import accountManager from "@/services/accounts"; import { @@ -58,7 +59,7 @@ export interface ZapWindowProps { } // Default preset amounts in sats -const DEFAULT_PRESETS = [21, 100, 500, 1000, 5000, 10000]; +const DEFAULT_PRESETS = [21, 420, 2100, 42000]; // LocalStorage keys const STORAGE_KEY_CUSTOM_AMOUNTS = "grimoire_zap_custom_amounts"; @@ -187,7 +188,7 @@ export function ZapWindow({ } }; - // Generate QR code for invoice + // Generate QR code for invoice with optional profile picture overlay const generateQrCode = async (invoiceText: string) => { try { const qrDataUrl = await QRCode.toDataURL(invoiceText, { @@ -198,7 +199,70 @@ export function ZapWindow({ light: "#FFFFFF", }, }); - return qrDataUrl; + + // If profile has picture, overlay it in the center + const profilePicUrl = recipientProfile?.picture; + if (!profilePicUrl) { + return qrDataUrl; + } + + // Create canvas to overlay profile picture + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + if (!ctx) return qrDataUrl; + + // Load QR code image + const qrImage = new Image(); + await new Promise((resolve, reject) => { + qrImage.onload = resolve; + qrImage.onerror = reject; + qrImage.src = qrDataUrl; + }); + + canvas.width = qrImage.width; + canvas.height = qrImage.height; + + // Draw QR code + ctx.drawImage(qrImage, 0, 0); + + // Load and draw profile picture + const profileImage = new Image(); + profileImage.crossOrigin = "anonymous"; + + await new Promise((resolve) => { + profileImage.onload = resolve; + profileImage.onerror = () => resolve(null); // Silently fail if image doesn't load + profileImage.src = profilePicUrl; + }); + + // Only overlay if image loaded successfully + if (profileImage.complete && profileImage.naturalHeight !== 0) { + const size = canvas.width * 0.25; // 25% of QR code size + const x = (canvas.width - size) / 2; + const y = (canvas.height - size) / 2; + + // Draw white background circle + ctx.fillStyle = "#FFFFFF"; + ctx.beginPath(); + ctx.arc( + canvas.width / 2, + canvas.height / 2, + size / 2 + 4, + 0, + 2 * Math.PI, + ); + ctx.fill(); + + // Clip to circle for profile picture + ctx.save(); + ctx.beginPath(); + ctx.arc(canvas.width / 2, canvas.height / 2, size / 2, 0, 2 * Math.PI); + ctx.clip(); + ctx.drawImage(profileImage, x, y, size, size); + ctx.restore(); + } + + return canvas.toDataURL(); } catch (error) { console.error("QR code generation error:", error); throw new Error("Failed to generate QR code"); @@ -413,7 +477,7 @@ export function ZapWindow({ {/* Header */}
- Zap {recipientName} + Zap
Scan with your Lightning wallet or copy the invoice @@ -451,24 +515,14 @@ export function ZapWindow({
{/* Actions */} -
- - -
+ {/* Retry with wallet button if payment timed out */} {paymentTimedOut && @@ -502,7 +556,9 @@ export function ZapWindow({ {/* Show recipient info if not zapping an event */} {!event && (
-
{recipientName}
+
+ +
{recipientProfile?.lud16 && (
{recipientProfile.lud16}