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.
This commit is contained in:
Claude
2026-01-18 22:13:26 +00:00
parent d3201c4b4b
commit a0df11c522

View File

@@ -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 */}
<div className="text-center space-y-2">
<div className="text-2xl font-semibold">
Zap {recipientName}
Zap <UserName pubkey={recipientPubkey} />
</div>
<div className="text-sm text-muted-foreground">
Scan with your Lightning wallet or copy the invoice
@@ -451,24 +515,14 @@ export function ZapWindow({
</div>
{/* Actions */}
<div className="flex gap-2">
<Button
variant="outline"
className="flex-1"
onClick={() => openInWallet(invoice)}
>
<ExternalLink className="size-4 mr-2" />
Open in Wallet
</Button>
<Button
variant="outline"
className="flex-1"
onClick={() => copyToClipboard(invoice)}
>
<Copy className="size-4 mr-2" />
Copy Invoice
</Button>
</div>
<Button
variant="outline"
className="w-full"
onClick={() => openInWallet(invoice)}
>
<ExternalLink className="size-4 mr-2" />
Open in Wallet
</Button>
{/* 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 && (
<div className="text-center space-y-2 py-4">
<div className="text-2xl font-semibold">{recipientName}</div>
<div className="text-2xl font-semibold">
<UserName pubkey={recipientPubkey} />
</div>
{recipientProfile?.lud16 && (
<div className="text-sm text-muted-foreground font-mono">
{recipientProfile.lud16}