This commit is contained in:
Chad Curtis 2025-07-13 20:12:42 +00:00
parent 8c34dbc8d5
commit 7eecec7506
5 changed files with 18 additions and 161 deletions

View File

@ -10,7 +10,6 @@ interface ZapButtonProps {
target: Event; target: Event;
className?: string; className?: string;
showCount?: boolean; showCount?: boolean;
// New: option to pass pre-fetched zap data (for batch mode)
zapData?: { count: number; totalSats: number; isLoading?: boolean }; zapData?: { count: number; totalSats: number; isLoading?: boolean };
} }

View File

@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { Zap, Copy, Sparkle, Sparkles, Star, Rocket, Wallet, Globe } from 'lucide-react'; import { Zap, Copy, Sparkle, Sparkles, Star, Rocket } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Dialog, Dialog,
@ -40,24 +40,7 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) {
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const { data: author } = useAuthor(target.pubkey); const { data: author } = useAuthor(target.pubkey);
const { toast } = useToast(); const { toast } = useToast();
const { webln, activeNWC, hasWebLN, hasNWC, detectWebLN } = useWallet(); const { webln, activeNWC, hasWebLN, detectWebLN } = useWallet();
// Debug logging
useEffect(() => {
console.debug('ZapDialog wallet status:', { hasWebLN, hasNWC, activeNWC: !!activeNWC });
}, [hasWebLN, hasNWC, activeNWC]);
// Additional debug logging when dialog opens
useEffect(() => {
if (open) {
console.debug('ZapDialog opened with wallet status:', {
hasWebLN,
hasNWC,
activeNWC: activeNWC ? { alias: activeNWC.alias, isConnected: activeNWC.isConnected } : null
});
}
}, [open, hasWebLN, hasNWC, activeNWC]);
const { zap, isZapping, invoice, setInvoice } = useZaps(target, webln, activeNWC, () => setOpen(false)); const { zap, isZapping, invoice, setInvoice } = useZaps(target, webln, activeNWC, () => setOpen(false));
const [amount, setAmount] = useState<number | string>(100); const [amount, setAmount] = useState<number | string>(100);
const [comment, setComment] = useState<string>(''); const [comment, setComment] = useState<string>('');
@ -104,7 +87,7 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) {
zap(finalAmount, comment); zap(finalAmount, comment);
}; };
if (!user || user.pubkey === target.pubkey || !author?.metadata?.lud16) { if (!user || user.pubkey === target.pubkey || !author?.metadata?.lud06 && !author?.metadata?.lud16) {
return null; return null;
} }
@ -141,28 +124,6 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) {
</div> </div>
) : ( ) : (
<> <>
{/* Payment Method Indicator */}
<div className="flex items-center justify-center py-2 px-1">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
{hasNWC ? (
<>
<Wallet className="h-4 w-4 text-green-600" />
<span>Wallet Connected</span>
</>
) : hasWebLN ? (
<>
<Globe className="h-4 w-4 text-blue-600" />
<span>WebLN Available</span>
</>
) : (
<>
<Copy className="h-4 w-4" />
<span>Manual Payment</span>
</>
)}
</div>
</div>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<ToggleGroup <ToggleGroup
type="single" type="single"

View File

@ -25,8 +25,6 @@ export function useNWCInternal() {
const [activeConnection, setActiveConnection] = useLocalStorage<string | null>('nwc-active-connection', null); const [activeConnection, setActiveConnection] = useLocalStorage<string | null>('nwc-active-connection', null);
const [connectionInfo, setConnectionInfo] = useState<Record<string, NWCInfo>>({}); const [connectionInfo, setConnectionInfo] = useState<Record<string, NWCInfo>>({});
// Use connections directly - no filtering needed
// Parse and validate NWC URI // Parse and validate NWC URI
const parseNWCUri = (uri: string): { connectionString: string } | null => { const parseNWCUri = (uri: string): { connectionString: string } | null => {
try { try {
@ -70,8 +68,6 @@ export function useNWCInternal() {
} }
try { try {
console.debug('Testing NWC connection:', { uri: uri.substring(0, 50) + '...' });
// Test the connection by creating an LN client with timeout // Test the connection by creating an LN client with timeout
const testPromise = new Promise((resolve, reject) => { const testPromise = new Promise((resolve, reject) => {
try { try {
@ -81,18 +77,15 @@ export function useNWCInternal() {
reject(error); reject(error);
} }
}); });
const timeoutPromise = new Promise((_, reject) => { const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Connection test timeout')), 10000); setTimeout(() => reject(new Error('Connection test timeout')), 10000);
}); });
await Promise.race([testPromise, timeoutPromise]) as LN;
const _client = await Promise.race([testPromise, timeoutPromise]) as LN;
const connection: NWCConnection = { const connection: NWCConnection = {
connectionString: parsed.connectionString, connectionString: parsed.connectionString,
alias: alias || 'NWC Wallet', alias: alias || 'NWC Wallet',
isConnected: true, isConnected: true,
// Don't store the client, create fresh ones for each payment
}; };
// Store basic connection info // Store basic connection info
@ -107,33 +100,9 @@ export function useNWCInternal() {
const newConnections = [...connections, connection]; const newConnections = [...connections, connection];
setConnections(newConnections); setConnections(newConnections);
console.debug('NWC connection added:', {
alias: connection.alias,
totalConnections: newConnections.length,
connectionString: parsed.connectionString.substring(0, 50) + '...',
isConnected: connection.isConnected
});
// Set as active if it's the first connection or no active connection is set // Set as active if it's the first connection or no active connection is set
if (connections.length === 0 || !activeConnection) { if (connections.length === 0 || !activeConnection)
console.debug('Setting as active connection:', {
alias: connection.alias,
connectionString: parsed.connectionString.substring(0, 50) + '...',
previousActiveConnection: activeConnection
});
setActiveConnection(parsed.connectionString); setActiveConnection(parsed.connectionString);
console.debug('Active connection set to:', parsed.connectionString.substring(0, 50) + '...');
}
console.debug('NWC connection successful');
// Force a small delay to ensure state updates are processed
setTimeout(() => {
console.debug('Post-connection state check:', {
connectionsLength: connections.length + 1, // +1 because we just added one
newConnectionAlias: connection.alias
});
}, 100);
toast({ toast({
title: 'Wallet connected', title: 'Wallet connected',
@ -159,14 +128,9 @@ export function useNWCInternal() {
const filtered = connections.filter(c => c.connectionString !== connectionString); const filtered = connections.filter(c => c.connectionString !== connectionString);
setConnections(filtered); setConnections(filtered);
console.debug('NWC connection removed:', {
remainingConnections: filtered.length
});
if (activeConnection === connectionString) { if (activeConnection === connectionString) {
const newActive = filtered.length > 0 ? filtered[0].connectionString : null; const newActive = filtered.length > 0 ? filtered[0].connectionString : null;
setActiveConnection(newActive); setActiveConnection(newActive);
console.debug('Active connection changed:', { newActive });
} }
setConnectionInfo(prev => { setConnectionInfo(prev => {
@ -183,15 +147,8 @@ export function useNWCInternal() {
// Get active connection // Get active connection
const getActiveConnection = useCallback((): NWCConnection | null => { const getActiveConnection = useCallback((): NWCConnection | null => {
console.debug('getActiveConnection called:', {
activeConnection,
connectionsLength: connections.length,
connections: connections.map(c => ({ alias: c.alias, connectionString: c.connectionString.substring(0, 50) + '...' }))
});
// If no active connection is set but we have connections, set the first one as active // If no active connection is set but we have connections, set the first one as active
if (!activeConnection && connections.length > 0) { if (!activeConnection && connections.length > 0) {
console.debug('Setting first connection as active:', connections[0].alias);
setActiveConnection(connections[0].connectionString); setActiveConnection(connections[0].connectionString);
return connections[0]; return connections[0];
} }
@ -202,7 +159,6 @@ export function useNWCInternal() {
} }
const found = connections.find(c => c.connectionString === activeConnection); const found = connections.find(c => c.connectionString === activeConnection);
console.debug('Found active connection:', found ? found.alias : 'null');
return found || null; return found || null;
}, [activeConnection, connections, setActiveConnection]); }, [activeConnection, connections, setActiveConnection]);
@ -218,7 +174,6 @@ export function useNWCInternal() {
// Always create a fresh client for each payment to avoid stale connections // Always create a fresh client for each payment to avoid stale connections
let client: LN; let client: LN;
try { try {
console.debug('Creating fresh NWC client for payment...');
client = new LN(connection.connectionString); client = new LN(connection.connectionString);
} catch (error) { } catch (error) {
console.error('Failed to create NWC client:', error); console.error('Failed to create NWC client:', error);
@ -226,20 +181,13 @@ export function useNWCInternal() {
} }
try { try {
console.debug('Sending payment via NWC SDK:', {
invoice: invoice.substring(0, 50) + '...',
connectionAlias: connection.alias
});
// Add timeout to prevent hanging // Add timeout to prevent hanging
const timeoutPromise = new Promise((_, reject) => { const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Payment timeout after 30 seconds')), 30000); setTimeout(() => reject(new Error('Payment timeout after 15 seconds')), 15);
}); });
const paymentPromise = client.pay(invoice); const paymentPromise = client.pay(invoice);
const response = await Promise.race([paymentPromise, timeoutPromise]) as { preimage: string }; const response = await Promise.race([paymentPromise, timeoutPromise]) as { preimage: string };
console.debug('Payment successful:', { preimage: response.preimage });
return response; return response;
} catch (error) { } catch (error) {
console.error('NWC payment failed:', error); console.error('NWC payment failed:', error);
@ -279,8 +227,6 @@ export function useNWCInternal() {
} }
try { try {
console.debug('Testing NWC connection...', { alias: connection.alias });
// Create a fresh client for testing // Create a fresh client for testing
const testPromise = new Promise((resolve, reject) => { const testPromise = new Promise((resolve, reject) => {
try { try {
@ -294,10 +240,8 @@ export function useNWCInternal() {
const timeoutPromise = new Promise((_, reject) => { const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Connection test timeout')), 5000); setTimeout(() => reject(new Error('Connection test timeout')), 5000);
}); });
await Promise.race([testPromise, timeoutPromise]); await Promise.race([testPromise, timeoutPromise]);
console.debug('NWC connection test successful');
return true; return true;
} catch (error) { } catch (error) {
console.error('NWC connection test failed:', error); console.error('NWC connection test failed:', error);
@ -305,8 +249,6 @@ export function useNWCInternal() {
} }
}, []); }, []);
return { return {
connections, connections,
activeConnection, activeConnection,

View File

@ -44,7 +44,7 @@ export function useWallet() {
} }
}, [webln, isDetecting]); }, [webln, isDetecting]);
// Only auto-detect once on mount, don't spam detection // Only auto-detect once on mount
useEffect(() => { useEffect(() => {
if (!hasAttemptedDetection) { if (!hasAttemptedDetection) {
detectWebLN(); detectWebLN();

View File

@ -24,7 +24,6 @@ function parseNWCUri(uri: string): NWCConnection | null {
const walletPubkey = url.pathname.replace('//', ''); const walletPubkey = url.pathname.replace('//', '');
const secret = url.searchParams.get('secret'); const secret = url.searchParams.get('secret');
const relayParam = url.searchParams.getAll('relay'); const relayParam = url.searchParams.getAll('relay');
const _lud16 = url.searchParams.get('lud16') || undefined;
if (!walletPubkey || !secret || relayParam.length === 0) { if (!walletPubkey || !secret || relayParam.length === 0) {
return null; return null;
@ -194,7 +193,7 @@ export function useZaps(
} }
try { try {
if (!author.data || !author.data?.metadata) { if (!author.data || !author.data?.metadata || !author.data?.event ) {
toast({ toast({
title: 'Author not found', title: 'Author not found',
description: 'Could not find the author of this item.', description: 'Could not find the author of this item.',
@ -204,8 +203,8 @@ export function useZaps(
return; return;
} }
const { lud16 } = author.data.metadata; const { lud06, lud16 } = author.data.metadata;
if (!lud16) { if (!lud06 && !lud16) {
toast({ toast({
title: 'Lightning address not found', title: 'Lightning address not found',
description: 'The author does not have a lightning address configured.', description: 'The author does not have a lightning address configured.',
@ -216,7 +215,7 @@ export function useZaps(
} }
// Get zap endpoint using the old reliable method // Get zap endpoint using the old reliable method
const zapEndpoint = await nip57.getZapEndpoint(author.data.event as Event); const zapEndpoint = await nip57.getZapEndpoint(author.data.event);
if (!zapEndpoint) { if (!zapEndpoint) {
toast({ toast({
title: 'Zap endpoint not found', title: 'Zap endpoint not found',
@ -227,30 +226,20 @@ export function useZaps(
return; return;
} }
// Create zap request
const zapAmount = amount * 1000; // convert to millisats const zapAmount = amount * 1000; // convert to millisats
const relays = [config.relayUrl];
// Create zap request (unsigned, like the old implementation)
const zapRequest = nip57.makeZapRequest({ const zapRequest = nip57.makeZapRequest({
profile: primaryTarget.pubkey, profile: primaryTarget.pubkey,
event: primaryTarget.id, event: primaryTarget,
amount: zapAmount, amount: zapAmount,
relays, relays: [config.relayUrl],
comment: comment, comment
}); });
// Handle addressable events
if (primaryTarget.kind >= 30000 && primaryTarget.kind < 40000) {
const identifier = primaryTarget.tags.find((t) => t[0] === 'd')?.[1] || '';
zapRequest.tags.push(["a", `${primaryTarget.kind}:${primaryTarget.pubkey}:${identifier}`]);
zapRequest.tags = zapRequest.tags.filter(t => t[0] !== 'e');
}
// Sign and publish the zap request // Sign and publish the zap request
publishEvent(zapRequest, { publishEvent(zapRequest, {
onSuccess: async (event) => { onSuccess: async (event) => {
try { try {
// Use the old fetch method - more reliable than LNURL validation
const res = await fetch(`${zapEndpoint}?amount=${zapAmount}&nostr=${encodeURI(JSON.stringify(event))}`); const res = await fetch(`${zapEndpoint}?amount=${zapAmount}&nostr=${encodeURI(JSON.stringify(event))}`);
const responseData = await res.json(); const responseData = await res.json();
@ -266,38 +255,10 @@ export function useZaps(
// Get the current active NWC connection dynamically // Get the current active NWC connection dynamically
const currentNWCConnection = getActiveConnection(); const currentNWCConnection = getActiveConnection();
console.debug('Zap payment - detailed state check:', {
// Raw state
connectionsLength: connections.length,
activeConnectionString: activeConnection ? activeConnection.substring(0, 50) + '...' : null,
// Connection details
connections: connections.map(c => ({
alias: c.alias,
isConnected: c.isConnected,
connectionString: c.connectionString.substring(0, 50) + '...'
})),
// getActiveConnection result
currentNWCConnection: currentNWCConnection ? {
alias: currentNWCConnection.alias,
isConnected: currentNWCConnection.isConnected,
connectionString: currentNWCConnection.connectionString.substring(0, 50) + '...'
} : null
});
// Try NWC first if available and properly connected // Try NWC first if available and properly connected
if (currentNWCConnection && currentNWCConnection.connectionString && currentNWCConnection.isConnected) { if (currentNWCConnection && currentNWCConnection.connectionString && currentNWCConnection.isConnected) {
try { try {
console.debug('Attempting NWC payment...', { await sendPayment(currentNWCConnection, newInvoice);
amount,
alias: currentNWCConnection.alias,
invoiceLength: newInvoice.length
});
const response = await sendPayment(currentNWCConnection, newInvoice);
console.debug('NWC payment successful:', { preimage: response.preimage });
// Clear states immediately on success // Clear states immediately on success
setIsZapping(false); setIsZapping(false);
@ -322,10 +283,7 @@ export function useZaps(
variant: 'destructive', variant: 'destructive',
}); });
} }
} } else if (webln) { // Try WebLN next
// Fallback to WebLN or manual payment
if (webln) {
await webln.sendPayment(newInvoice); await webln.sendPayment(newInvoice);
// Clear states immediately on success // Clear states immediately on success
@ -339,7 +297,7 @@ export function useZaps(
// Close dialog last to ensure clean state // Close dialog last to ensure clean state
onZapSuccess?.(); onZapSuccess?.();
} else { } else { // Default - show QR code and manual Lightning URI
setInvoice(newInvoice); setInvoice(newInvoice);
setIsZapping(false); setIsZapping(false);
} }
@ -376,7 +334,6 @@ export function useZaps(
}; };
return { return {
// Legacy single-target API (for backward compatibility)
zaps, zaps,
...query, ...query,
zap, zap,
@ -384,8 +341,6 @@ export function useZaps(
invoice, invoice,
setInvoice, setInvoice,
parseNWCUri, parseNWCUri,
// New batch API
zapData, zapData,
isBatchMode, isBatchMode,