mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-06 18:51:21 +02:00
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:
@@ -32,6 +32,7 @@ import eventStore from "@/services/event-store";
|
|||||||
import { useWallet } from "@/hooks/useWallet";
|
import { useWallet } from "@/hooks/useWallet";
|
||||||
import { getDisplayName } from "@/lib/nostr-utils";
|
import { getDisplayName } from "@/lib/nostr-utils";
|
||||||
import { KindRenderer } from "./nostr/kinds";
|
import { KindRenderer } from "./nostr/kinds";
|
||||||
|
import { UserName } from "./nostr/UserName";
|
||||||
import type { EventPointer, AddressPointer } from "@/lib/open-parser";
|
import type { EventPointer, AddressPointer } from "@/lib/open-parser";
|
||||||
import accountManager from "@/services/accounts";
|
import accountManager from "@/services/accounts";
|
||||||
import {
|
import {
|
||||||
@@ -58,7 +59,7 @@ export interface ZapWindowProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default preset amounts in sats
|
// Default preset amounts in sats
|
||||||
const DEFAULT_PRESETS = [21, 100, 500, 1000, 5000, 10000];
|
const DEFAULT_PRESETS = [21, 420, 2100, 42000];
|
||||||
|
|
||||||
// LocalStorage keys
|
// LocalStorage keys
|
||||||
const STORAGE_KEY_CUSTOM_AMOUNTS = "grimoire_zap_custom_amounts";
|
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) => {
|
const generateQrCode = async (invoiceText: string) => {
|
||||||
try {
|
try {
|
||||||
const qrDataUrl = await QRCode.toDataURL(invoiceText, {
|
const qrDataUrl = await QRCode.toDataURL(invoiceText, {
|
||||||
@@ -198,7 +199,70 @@ export function ZapWindow({
|
|||||||
light: "#FFFFFF",
|
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) {
|
} catch (error) {
|
||||||
console.error("QR code generation error:", error);
|
console.error("QR code generation error:", error);
|
||||||
throw new Error("Failed to generate QR code");
|
throw new Error("Failed to generate QR code");
|
||||||
@@ -413,7 +477,7 @@ export function ZapWindow({
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="text-center space-y-2">
|
<div className="text-center space-y-2">
|
||||||
<div className="text-2xl font-semibold">
|
<div className="text-2xl font-semibold">
|
||||||
Zap {recipientName}
|
Zap <UserName pubkey={recipientPubkey} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
Scan with your Lightning wallet or copy the invoice
|
Scan with your Lightning wallet or copy the invoice
|
||||||
@@ -451,24 +515,14 @@ export function ZapWindow({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-2">
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
variant="outline"
|
className="w-full"
|
||||||
className="flex-1"
|
onClick={() => openInWallet(invoice)}
|
||||||
onClick={() => openInWallet(invoice)}
|
>
|
||||||
>
|
<ExternalLink className="size-4 mr-2" />
|
||||||
<ExternalLink className="size-4 mr-2" />
|
Open in Wallet
|
||||||
Open in Wallet
|
</Button>
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="flex-1"
|
|
||||||
onClick={() => copyToClipboard(invoice)}
|
|
||||||
>
|
|
||||||
<Copy className="size-4 mr-2" />
|
|
||||||
Copy Invoice
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Retry with wallet button if payment timed out */}
|
{/* Retry with wallet button if payment timed out */}
|
||||||
{paymentTimedOut &&
|
{paymentTimedOut &&
|
||||||
@@ -502,7 +556,9 @@ export function ZapWindow({
|
|||||||
{/* Show recipient info if not zapping an event */}
|
{/* Show recipient info if not zapping an event */}
|
||||||
{!event && (
|
{!event && (
|
||||||
<div className="text-center space-y-2 py-4">
|
<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 && (
|
{recipientProfile?.lud16 && (
|
||||||
<div className="text-sm text-muted-foreground font-mono">
|
<div className="text-sm text-muted-foreground font-mono">
|
||||||
{recipientProfile.lud16}
|
{recipientProfile.lud16}
|
||||||
|
|||||||
Reference in New Issue
Block a user