mirror of
https://gitlab.com/soapbox-pub/mkstack.git
synced 2025-08-27 13:09:22 +00:00
messy, nwc support +_wallet fixes
This commit is contained in:
parent
11ac73776c
commit
45ff37e752
54
package-lock.json
generated
54
package-lock.json
generated
@ -8,6 +8,8 @@
|
||||
"name": "mkstack",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.2.6",
|
||||
"@getalby/sdk": "^5.1.1",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.1",
|
||||
"@nostrify/react": "npm:@jsr/nostrify__react@^0.2.5",
|
||||
@ -993,6 +995,45 @@
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@fontsource-variable/inter": {
|
||||
"version": "5.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.2.6.tgz",
|
||||
"integrity": "sha512-jks/bficUPQ9nn7GvXvHtlQIPudW7Wx8CrlZoY8bhxgeobNxlQan8DclUJuYF2loYRrGpfrhCIZZspXYysiVGg==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@getalby/lightning-tools": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@getalby/lightning-tools/-/lightning-tools-5.2.0.tgz",
|
||||
"integrity": "sha512-8kBvENBTMh541VjGKhw3I29+549/C02gLSh3AQaMfoMNSZaMxfQW+7dcMcc7vbFaCKEcEe18ST5bUveTRBuXCQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"type": "lightning",
|
||||
"url": "lightning:hello@getalby.com"
|
||||
}
|
||||
},
|
||||
"node_modules/@getalby/sdk": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@getalby/sdk/-/sdk-5.1.1.tgz",
|
||||
"integrity": "sha512-t/kg2ljPx86qRYKqEVc5VYhDICFKtVPRlQKIz5cI/AqOLYVguLJz1AkQlDBaiOz2PW5FxoyGlLkTGmX7ONHH/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@getalby/lightning-tools": "^5.1.2",
|
||||
"nostr-tools": "2.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"type": "lightning",
|
||||
"url": "lightning:hello@getalby.com"
|
||||
}
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz",
|
||||
@ -6147,9 +6188,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nostr-tools": {
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.13.0.tgz",
|
||||
"integrity": "sha512-A1arGsvpULqVK0NmZQqK1imwaCiPm8gcG/lo+cTax2NbNqBEYsuplbqAFdVqcGHEopmkByYbTwF76x25+oEbew==",
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.15.0.tgz",
|
||||
"integrity": "sha512-Jj/+UFbu3JbTAWP4ipPFNuyD4W5eVRBNAP+kmnoRCYp3bLmTrlQ0Qhs5O1xSQJTFpjdZqoS0zZOUKdxUdjc+pw==",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"@noble/ciphers": "^0.5.1",
|
||||
@ -6157,9 +6198,7 @@
|
||||
"@noble/hashes": "1.3.1",
|
||||
"@scure/base": "1.1.1",
|
||||
"@scure/bip32": "1.3.1",
|
||||
"@scure/bip39": "1.2.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@scure/bip39": "1.2.1",
|
||||
"nostr-wasm": "0.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@ -6175,8 +6214,7 @@
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
|
||||
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nwsapi": {
|
||||
"version": "2.2.20",
|
||||
|
@ -10,6 +10,8 @@
|
||||
"deploy": "npm run build && npx -y nostr-deploy-cli deploy --skip-setup"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.2.6",
|
||||
"@getalby/sdk": "^5.1.1",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.1",
|
||||
"@nostrify/react": "npm:@jsr/nostrify__react@^0.2.5",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Wallet, Plus, Trash2, Zap, Globe, Settings, CheckCircle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@ -15,7 +15,7 @@ import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { useNWC } from '@/hooks/useNWC';
|
||||
import { useNWC } from '@/hooks/useNWCContext';
|
||||
import { useWallet } from '@/hooks/useWallet';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
|
||||
@ -40,9 +40,23 @@ export function WalletModal({ children, className }: WalletModalProps) {
|
||||
setActiveConnection
|
||||
} = useNWC();
|
||||
|
||||
const { hasWebLN, hasNWC, isDetecting } = useWallet();
|
||||
const { hasWebLN, isDetecting } = useWallet();
|
||||
|
||||
// Calculate hasNWC directly from connections to ensure reactivity
|
||||
const hasNWC = connections.length > 0 && connections.some(c => c.isConnected);
|
||||
const { toast } = useToast();
|
||||
|
||||
// Debug logging for wallet modal status
|
||||
useEffect(() => {
|
||||
console.debug('WalletModal status:', {
|
||||
hasWebLN,
|
||||
hasNWC,
|
||||
connectionsCount: connections.length,
|
||||
connectionsDetails: connections.map(c => ({ alias: c.alias, isConnected: c.isConnected })),
|
||||
isDetecting
|
||||
});
|
||||
}, [hasWebLN, hasNWC, connections, isDetecting]);
|
||||
|
||||
const handleAddConnection = async () => {
|
||||
if (!connectionUri.trim()) {
|
||||
toast({
|
||||
@ -53,33 +67,61 @@ export function WalletModal({ children, className }: WalletModalProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug('WalletModal: Before adding connection', {
|
||||
currentConnections: connections.length,
|
||||
hasNWC
|
||||
});
|
||||
|
||||
setIsConnecting(true);
|
||||
try {
|
||||
const success = await addConnection(connectionUri.trim(), alias.trim() || undefined);
|
||||
if (success) {
|
||||
console.debug('WalletModal: Connection added successfully', {
|
||||
newConnections: connections.length,
|
||||
hasNWC
|
||||
});
|
||||
setConnectionUri('');
|
||||
setAlias('');
|
||||
setAddDialogOpen(false);
|
||||
|
||||
// Force a small delay to check state after React updates
|
||||
setTimeout(() => {
|
||||
console.debug('WalletModal: Post-add state check', {
|
||||
connectionsLength: connections.length,
|
||||
hasNWC
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveConnection = (walletPubkey: string) => {
|
||||
removeConnection(walletPubkey);
|
||||
const handleRemoveConnection = (connectionString: string) => {
|
||||
console.debug('WalletModal: Before removing connection', {
|
||||
currentConnections: connections.length,
|
||||
hasNWC
|
||||
});
|
||||
|
||||
removeConnection(connectionString);
|
||||
|
||||
// Force a small delay to check state after React updates
|
||||
setTimeout(() => {
|
||||
console.debug('WalletModal: Post-remove state check', {
|
||||
connectionsLength: connections.length,
|
||||
hasNWC
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleSetActive = (walletPubkey: string) => {
|
||||
setActiveConnection(walletPubkey);
|
||||
const handleSetActive = (connectionString: string) => {
|
||||
setActiveConnection(connectionString);
|
||||
toast({
|
||||
title: 'Active wallet changed',
|
||||
description: 'The selected wallet is now active for zaps.',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
@ -208,11 +250,11 @@ export function WalletModal({ children, className }: WalletModalProps) {
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{connections.map((connection) => {
|
||||
const info = connectionInfo[connection.walletPubkey];
|
||||
const isActive = activeConnection === connection.walletPubkey;
|
||||
const info = connectionInfo[connection.connectionString];
|
||||
const isActive = activeConnection === connection.connectionString;
|
||||
|
||||
return (
|
||||
<div key={connection.walletPubkey} className={`flex items-center justify-between p-3 border rounded-lg ${isActive ? 'ring-2 ring-primary' : ''}`}>
|
||||
<div key={connection.connectionString} className={`flex items-center justify-between p-3 border rounded-lg ${isActive ? 'ring-2 ring-primary' : ''}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<Settings className="h-4 w-4 text-muted-foreground" />
|
||||
<div>
|
||||
@ -220,7 +262,7 @@ export function WalletModal({ children, className }: WalletModalProps) {
|
||||
{connection.alias || info?.alias || 'Lightning Wallet'}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{connection.walletPubkey.slice(0, 16)}...
|
||||
NWC Connection
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -230,7 +272,7 @@ export function WalletModal({ children, className }: WalletModalProps) {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleSetActive(connection.walletPubkey)}
|
||||
onClick={() => handleSetActive(connection.connectionString)}
|
||||
>
|
||||
<Zap className="h-3 w-3" />
|
||||
</Button>
|
||||
@ -238,7 +280,7 @@ export function WalletModal({ children, className }: WalletModalProps) {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleRemoveConnection(connection.walletPubkey)}
|
||||
onClick={() => handleRemoveConnection(connection.connectionString)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
|
47
src/components/ZapButton.tsx
Normal file
47
src/components/ZapButton.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { ZapDialog } from '@/components/ZapDialog';
|
||||
import { useZaps } from '@/hooks/useZaps';
|
||||
import { useWallet } from '@/hooks/useWallet';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import type { Event } from 'nostr-tools';
|
||||
|
||||
interface ZapButtonProps {
|
||||
target: Event;
|
||||
className?: string;
|
||||
showCount?: boolean;
|
||||
}
|
||||
|
||||
export function ZapButton({ target, className = "text-xs ml-1", showCount = true }: ZapButtonProps) {
|
||||
const { user } = useCurrentUser();
|
||||
const { data: author } = useAuthor(target.pubkey);
|
||||
const { webln, activeNWC } = useWallet();
|
||||
const { zaps } = useZaps(target, webln, activeNWC);
|
||||
|
||||
// Don't show zap button if user is not logged in, is the author, or author has no lightning address
|
||||
if (!user || user.pubkey === target.pubkey || (!author?.metadata?.lud16 && !author?.metadata?.lud06)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const zapCount = zaps?.length || 0;
|
||||
const totalSats = zaps?.reduce((total, zap) => {
|
||||
// Extract amount from amount tag
|
||||
const amountTag = zap.tags.find(([name]) => name === 'amount')?.[1];
|
||||
|
||||
if (amountTag) {
|
||||
return total + Math.floor(parseInt(amountTag) / 1000); // Convert millisats to sats
|
||||
}
|
||||
|
||||
// If no amount tag, don't count towards total
|
||||
return total;
|
||||
}, 0) || 0;
|
||||
|
||||
return (
|
||||
<ZapDialog target={target}>
|
||||
{showCount && zapCount > 0 && (
|
||||
<span className={className}>
|
||||
{totalSats > 0 ? `${totalSats.toLocaleString()}` : zapCount}
|
||||
</span>
|
||||
)}
|
||||
</ZapDialog>
|
||||
);
|
||||
}
|
@ -41,6 +41,23 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) {
|
||||
const { data: author } = useAuthor(target.pubkey);
|
||||
const { toast } = useToast();
|
||||
const { webln, activeNWC, hasWebLN, hasNWC, 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 [amount, setAmount] = useState<number | string>(100);
|
||||
const [comment, setComment] = useState<string>('');
|
||||
|
8
src/contexts/NWCContext.tsx
Normal file
8
src/contexts/NWCContext.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { useNWCInternal as useNWCHook } from '@/hooks/useNWC';
|
||||
import { NWCContext } from '@/hooks/useNWCContext';
|
||||
|
||||
export function NWCProvider({ children }: { children: ReactNode }) {
|
||||
const nwc = useNWCHook();
|
||||
return <NWCContext.Provider value={nwc}>{children}</NWCContext.Provider>;
|
||||
}
|
@ -1,16 +1,13 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { nip04 } from 'nostr-tools';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { LN } from '@getalby/sdk';
|
||||
|
||||
export interface NWCConnection {
|
||||
walletPubkey: string;
|
||||
secret: string;
|
||||
relayUrls: string[];
|
||||
lud16?: string;
|
||||
connectionString: string;
|
||||
alias?: string;
|
||||
isConnected: boolean;
|
||||
client?: LN;
|
||||
}
|
||||
|
||||
interface NWCInfo {
|
||||
@ -22,46 +19,37 @@ interface NWCInfo {
|
||||
notifications?: string[];
|
||||
}
|
||||
|
||||
export function useNWC() {
|
||||
const { nostr } = useNostr();
|
||||
export function useNWCInternal() {
|
||||
const { toast } = useToast();
|
||||
const { user } = useCurrentUser();
|
||||
const [connections, setConnections] = useLocalStorage<NWCConnection[]>('nwc-connections', []);
|
||||
const [activeConnection, setActiveConnection] = useLocalStorage<string | null>('nwc-active-connection', null);
|
||||
const [connectionInfo, setConnectionInfo] = useState<Record<string, NWCInfo>>({});
|
||||
|
||||
// Parse NWC URI
|
||||
const parseNWCUri = (uri: string): NWCConnection | null => {
|
||||
// Use connections directly - no filtering needed
|
||||
|
||||
// Parse and validate NWC URI
|
||||
const parseNWCUri = (uri: string): { connectionString: string } | null => {
|
||||
try {
|
||||
const url = new URL(uri);
|
||||
if (url.protocol !== 'nostr+walletconnect:') {
|
||||
console.debug('Parsing NWC URI:', { uri: uri.substring(0, 50) + '...' });
|
||||
|
||||
if (!uri.startsWith('nostr+walletconnect://') && !uri.startsWith('nostrwalletconnect://')) {
|
||||
console.error('Invalid NWC URI protocol:', { protocol: uri.split('://')[0] });
|
||||
return null;
|
||||
}
|
||||
|
||||
const walletPubkey = url.pathname.replace('//', '');
|
||||
const secret = url.searchParams.get('secret');
|
||||
const relayParam = url.searchParams.getAll('relay');
|
||||
const lud16 = url.searchParams.get('lud16') || undefined;
|
||||
|
||||
if (!walletPubkey || !secret || relayParam.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
walletPubkey,
|
||||
secret,
|
||||
relayUrls: relayParam,
|
||||
lud16,
|
||||
};
|
||||
} catch {
|
||||
// Basic validation - let the SDK handle the detailed parsing
|
||||
console.debug('NWC URI parsing successful');
|
||||
return { connectionString: uri };
|
||||
} catch (error) {
|
||||
console.error('Failed to parse NWC URI:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Add new connection
|
||||
const addConnection = async (uri: string, alias?: string): Promise<boolean> => {
|
||||
const connection = parseNWCUri(uri);
|
||||
if (!connection) {
|
||||
const parsed = parseNWCUri(uri);
|
||||
if (!parsed) {
|
||||
toast({
|
||||
title: 'Invalid NWC URI',
|
||||
description: 'Please check the connection string and try again.',
|
||||
@ -71,7 +59,7 @@ export function useNWC() {
|
||||
}
|
||||
|
||||
// Check if connection already exists
|
||||
const existingConnection = connections.find(c => c.walletPubkey === connection.walletPubkey);
|
||||
const existingConnection = connections.find(c => c.connectionString === parsed.connectionString);
|
||||
if (existingConnection) {
|
||||
toast({
|
||||
title: 'Connection already exists',
|
||||
@ -81,31 +69,85 @@ export function useNWC() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (alias) {
|
||||
connection.alias = alias;
|
||||
}
|
||||
|
||||
try {
|
||||
// Test connection by fetching info
|
||||
await fetchWalletInfo(connection);
|
||||
console.debug('Testing NWC connection:', { uri: uri.substring(0, 50) + '...' });
|
||||
|
||||
setConnections(prev => [...prev, connection]);
|
||||
// Test the connection by creating an LN client with timeout
|
||||
const testPromise = new Promise((resolve, reject) => {
|
||||
try {
|
||||
const client = new LN(parsed.connectionString);
|
||||
resolve(client);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Set as active if it's the first connection
|
||||
if (connections.length === 0) {
|
||||
setActiveConnection(connection.walletPubkey);
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Connection test timeout')), 10000);
|
||||
});
|
||||
|
||||
const _client = await Promise.race([testPromise, timeoutPromise]) as LN;
|
||||
|
||||
const connection: NWCConnection = {
|
||||
connectionString: parsed.connectionString,
|
||||
alias: alias || 'NWC Wallet',
|
||||
isConnected: true,
|
||||
// Don't store the client, create fresh ones for each payment
|
||||
};
|
||||
|
||||
// Store basic connection info
|
||||
setConnectionInfo(prev => ({
|
||||
...prev,
|
||||
[parsed.connectionString]: {
|
||||
alias: connection.alias,
|
||||
methods: ['pay_invoice'], // Assume basic payment capability
|
||||
},
|
||||
}));
|
||||
|
||||
const newConnections = [...connections, connection];
|
||||
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
|
||||
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);
|
||||
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({
|
||||
title: 'Wallet connected',
|
||||
description: `Successfully connected to ${alias || 'wallet'}.`,
|
||||
description: `Successfully connected to ${connection.alias}.`,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.error('NWC connection failed:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
|
||||
toast({
|
||||
title: 'Connection failed',
|
||||
description: 'Could not connect to the wallet. Please check your connection.',
|
||||
description: `Could not connect to the wallet: ${errorMessage}`,
|
||||
variant: 'destructive',
|
||||
});
|
||||
return false;
|
||||
@ -113,17 +155,23 @@ export function useNWC() {
|
||||
};
|
||||
|
||||
// Remove connection
|
||||
const removeConnection = (walletPubkey: string) => {
|
||||
setConnections(prev => prev.filter(c => c.walletPubkey !== walletPubkey));
|
||||
const removeConnection = (connectionString: string) => {
|
||||
const filtered = connections.filter(c => c.connectionString !== connectionString);
|
||||
setConnections(filtered);
|
||||
|
||||
if (activeConnection === walletPubkey) {
|
||||
const remaining = connections.filter(c => c.walletPubkey !== walletPubkey);
|
||||
setActiveConnection(remaining.length > 0 ? remaining[0].walletPubkey : null);
|
||||
console.debug('NWC connection removed:', {
|
||||
remainingConnections: filtered.length
|
||||
});
|
||||
|
||||
if (activeConnection === connectionString) {
|
||||
const newActive = filtered.length > 0 ? filtered[0].connectionString : null;
|
||||
setActiveConnection(newActive);
|
||||
console.debug('Active connection changed:', { newActive });
|
||||
}
|
||||
|
||||
setConnectionInfo(prev => {
|
||||
const newInfo = { ...prev };
|
||||
delete newInfo[walletPubkey];
|
||||
delete newInfo[connectionString];
|
||||
return newInfo;
|
||||
});
|
||||
|
||||
@ -134,156 +182,130 @@ export function useNWC() {
|
||||
};
|
||||
|
||||
// Get active connection
|
||||
const getActiveConnection = (): NWCConnection | null => {
|
||||
if (!activeConnection) return null;
|
||||
return connections.find(c => c.walletPubkey === activeConnection) || 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) + '...' }))
|
||||
});
|
||||
|
||||
// Send NWC request
|
||||
const sendNWCRequest = useCallback(async (
|
||||
// If no active connection is set but we have connections, set the first one as active
|
||||
if (!activeConnection && connections.length > 0) {
|
||||
console.debug('Setting first connection as active:', connections[0].alias);
|
||||
setActiveConnection(connections[0].connectionString);
|
||||
return connections[0];
|
||||
}
|
||||
|
||||
if (!activeConnection) {
|
||||
console.debug('No active connection and no connections');
|
||||
return null;
|
||||
}
|
||||
|
||||
const found = connections.find(c => c.connectionString === activeConnection);
|
||||
console.debug('Found active connection:', found ? found.alias : 'null');
|
||||
return found || null;
|
||||
}, [activeConnection, connections, setActiveConnection]);
|
||||
|
||||
// Send payment using the SDK
|
||||
const sendPayment = useCallback(async (
|
||||
connection: NWCConnection,
|
||||
request: { method: string; params: Record<string, unknown> }
|
||||
): Promise<{ result_type: string; error?: { code: string; message: string }; result?: unknown }> => {
|
||||
if (!user?.signer) {
|
||||
throw new Error('User not logged in or signer not available');
|
||||
invoice: string
|
||||
): Promise<{ preimage: string }> => {
|
||||
if (!connection.connectionString) {
|
||||
throw new Error('Invalid connection: missing connection string');
|
||||
}
|
||||
|
||||
// Create request event
|
||||
const requestEvent = {
|
||||
kind: 23194,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [['p', connection.walletPubkey]],
|
||||
content: await nip04.encrypt(connection.secret, connection.walletPubkey, JSON.stringify(request)),
|
||||
};
|
||||
|
||||
// Sign and publish request
|
||||
const signedRequest = await user.signer.signEvent(requestEvent);
|
||||
if (!signedRequest) {
|
||||
throw new Error('Failed to sign NWC request');
|
||||
}
|
||||
|
||||
// Publish to NWC relays
|
||||
// Always create a fresh client for each payment to avoid stale connections
|
||||
let client: LN;
|
||||
try {
|
||||
await nostr.event(signedRequest, {
|
||||
signal: AbortSignal.timeout(10000),
|
||||
relays: connection.relayUrls
|
||||
});
|
||||
console.debug('Creating fresh NWC client for payment...');
|
||||
client = new LN(connection.connectionString);
|
||||
} catch (error) {
|
||||
console.warn('Failed to publish NWC request:', error);
|
||||
throw new Error('Failed to publish NWC request');
|
||||
console.error('Failed to create NWC client:', error);
|
||||
throw new Error(`Failed to create NWC client: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
|
||||
// Listen for response
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('NWC request timeout'));
|
||||
}, 30000); // 30 second timeout
|
||||
try {
|
||||
console.debug('Sending payment via NWC SDK:', {
|
||||
invoice: invoice.substring(0, 50) + '...',
|
||||
connectionAlias: connection.alias
|
||||
});
|
||||
|
||||
// Query for response events
|
||||
const checkForResponse = async () => {
|
||||
// Add timeout to prevent hanging
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Payment timeout after 30 seconds')), 30000);
|
||||
});
|
||||
|
||||
const paymentPromise = client.pay(invoice);
|
||||
const response = await Promise.race([paymentPromise, timeoutPromise]) as { preimage: string };
|
||||
|
||||
console.debug('Payment successful:', { preimage: response.preimage });
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('NWC payment failed:', error);
|
||||
|
||||
// Provide more specific error messages
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('timeout')) {
|
||||
throw new Error('Payment timed out. Please try again.');
|
||||
} else if (error.message.includes('insufficient')) {
|
||||
throw new Error('Insufficient balance in connected wallet.');
|
||||
} else if (error.message.includes('invalid')) {
|
||||
throw new Error('Invalid invoice or connection. Please check your wallet.');
|
||||
} else {
|
||||
throw new Error(`Payment failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Payment failed with unknown error');
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Get wallet info (simplified since SDK doesn't expose getInfo)
|
||||
const getWalletInfo = useCallback(async (connection: NWCConnection): Promise<NWCInfo> => {
|
||||
// Return stored info or basic fallback
|
||||
const info = connectionInfo[connection.connectionString] || {
|
||||
alias: connection.alias,
|
||||
methods: ['pay_invoice'],
|
||||
};
|
||||
return info;
|
||||
}, [connectionInfo]);
|
||||
|
||||
// Test NWC connection
|
||||
const testNWCConnection = useCallback(async (connection: NWCConnection): Promise<boolean> => {
|
||||
if (!connection.connectionString) {
|
||||
console.error('NWC connection test failed: missing connection string');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
console.debug('Testing NWC connection...', { alias: connection.alias });
|
||||
|
||||
// Create a fresh client for testing
|
||||
const testPromise = new Promise((resolve, reject) => {
|
||||
try {
|
||||
const responseEvents = await nostr.query([
|
||||
{
|
||||
kinds: [23195],
|
||||
authors: [connection.walletPubkey],
|
||||
'#p': [user.pubkey],
|
||||
'#e': [signedRequest.id],
|
||||
since: Math.floor(Date.now() / 1000) - 60,
|
||||
},
|
||||
], { signal: AbortSignal.timeout(30000) });
|
||||
|
||||
for (const event of responseEvents) {
|
||||
try {
|
||||
const decrypted = await nip04.decrypt(
|
||||
connection.secret,
|
||||
connection.walletPubkey,
|
||||
event.content
|
||||
);
|
||||
const response = JSON.parse(decrypted);
|
||||
clearTimeout(timeout);
|
||||
resolve(response);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Failed to decrypt NWC response:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// If no response found, wait and try again
|
||||
setTimeout(checkForResponse, 2000);
|
||||
const client = new LN(connection.connectionString);
|
||||
resolve(client);
|
||||
} catch (error) {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Start checking for responses
|
||||
setTimeout(checkForResponse, 1000); // Wait 1 second before first check
|
||||
});
|
||||
}, [nostr, user]);
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Connection test timeout')), 5000);
|
||||
});
|
||||
|
||||
// Fetch wallet info
|
||||
const fetchWalletInfo = useCallback(async (connection: NWCConnection): Promise<NWCInfo> => {
|
||||
// First, try to get the info event (kind 13194)
|
||||
try {
|
||||
const infoEvents = await nostr.query([
|
||||
{
|
||||
kinds: [13194],
|
||||
authors: [connection.walletPubkey],
|
||||
limit: 1,
|
||||
}
|
||||
], { signal: AbortSignal.timeout(5000) });
|
||||
await Promise.race([testPromise, timeoutPromise]);
|
||||
|
||||
if (infoEvents.length > 0) {
|
||||
const infoEvent = infoEvents[0];
|
||||
const capabilities = infoEvent.content.split(' ');
|
||||
const notificationsTag = infoEvent.tags.find(tag => tag[0] === 'notifications');
|
||||
const notifications = notificationsTag ? notificationsTag[1].split(' ') : [];
|
||||
|
||||
const info: NWCInfo = {
|
||||
methods: capabilities,
|
||||
notifications,
|
||||
};
|
||||
|
||||
setConnectionInfo(prev => ({
|
||||
...prev,
|
||||
[connection.walletPubkey]: info,
|
||||
}));
|
||||
|
||||
return info;
|
||||
}
|
||||
console.debug('NWC connection test successful');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch NWC info event:', error);
|
||||
console.error('NWC connection test failed:', error);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Fallback: try to send a get_info request
|
||||
try {
|
||||
const response = await sendNWCRequest(connection, { method: 'get_info', params: {} });
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error.message);
|
||||
}
|
||||
|
||||
const info = response.result as NWCInfo;
|
||||
setConnectionInfo(prev => ({
|
||||
...prev,
|
||||
[connection.walletPubkey]: info,
|
||||
}));
|
||||
|
||||
return info;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch wallet info:', error);
|
||||
throw error;
|
||||
}
|
||||
}, [nostr, sendNWCRequest]);
|
||||
|
||||
// Fetch info for all connections on mount
|
||||
useEffect(() => {
|
||||
connections.forEach(connection => {
|
||||
if (!connectionInfo[connection.walletPubkey]) {
|
||||
fetchWalletInfo(connection).catch(console.error);
|
||||
}
|
||||
});
|
||||
}, [connections, connectionInfo, fetchWalletInfo]);
|
||||
|
||||
return {
|
||||
connections,
|
||||
@ -293,8 +315,9 @@ export function useNWC() {
|
||||
removeConnection,
|
||||
setActiveConnection,
|
||||
getActiveConnection,
|
||||
fetchWalletInfo,
|
||||
sendNWCRequest,
|
||||
sendPayment,
|
||||
getWalletInfo,
|
||||
parseNWCUri,
|
||||
testNWCConnection,
|
||||
};
|
||||
}
|
15
src/hooks/useNWCContext.ts
Normal file
15
src/hooks/useNWCContext.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { useContext } from 'react';
|
||||
import { createContext } from 'react';
|
||||
import { useNWCInternal } from '@/hooks/useNWC';
|
||||
|
||||
type NWCContextType = ReturnType<typeof useNWCInternal>;
|
||||
|
||||
export const NWCContext = createContext<NWCContextType | null>(null);
|
||||
|
||||
export function useNWC(): NWCContextType {
|
||||
const context = useContext(NWCContext);
|
||||
if (!context) {
|
||||
throw new Error('useNWC must be used within a NWCProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useNWC } from '@/hooks/useNWC';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useNWC } from '@/hooks/useNWCContext';
|
||||
import type { WebLNProvider } from 'webln';
|
||||
import { requestProvider } from 'webln';
|
||||
|
||||
@ -15,8 +15,10 @@ export interface WalletStatus {
|
||||
export function useWallet() {
|
||||
const [webln, setWebln] = useState<WebLNProvider | null>(null);
|
||||
const [isDetecting, setIsDetecting] = useState(false);
|
||||
const { getActiveConnection } = useNWC();
|
||||
const [hasAttemptedDetection, setHasAttemptedDetection] = useState(false);
|
||||
const { connections, getActiveConnection } = useNWC();
|
||||
|
||||
// Get the active connection directly - no memoization to avoid stale state
|
||||
const activeNWC = getActiveConnection();
|
||||
|
||||
// Detect WebLN
|
||||
@ -27,20 +29,27 @@ export function useWallet() {
|
||||
try {
|
||||
const provider = await requestProvider();
|
||||
setWebln(provider);
|
||||
setHasAttemptedDetection(true);
|
||||
return provider;
|
||||
} catch (error) {
|
||||
console.warn('WebLN not available:', error);
|
||||
// Only log the error if it's not the common "no provider" error
|
||||
if (error instanceof Error && !error.message.includes('no WebLN provider')) {
|
||||
console.warn('WebLN detection error:', error);
|
||||
}
|
||||
setWebln(null);
|
||||
setHasAttemptedDetection(true);
|
||||
return null;
|
||||
} finally {
|
||||
setIsDetecting(false);
|
||||
}
|
||||
}, [webln, isDetecting]);
|
||||
|
||||
// Auto-detect on mount
|
||||
// Only auto-detect once on mount, don't spam detection
|
||||
useEffect(() => {
|
||||
detectWebLN();
|
||||
}, [detectWebLN]);
|
||||
if (!hasAttemptedDetection) {
|
||||
detectWebLN();
|
||||
}
|
||||
}, [detectWebLN, hasAttemptedDetection]);
|
||||
|
||||
// Test WebLN connection
|
||||
const testWebLN = useCallback(async (): Promise<boolean> => {
|
||||
@ -55,6 +64,11 @@ export function useWallet() {
|
||||
}
|
||||
}, [webln]);
|
||||
|
||||
// Calculate status values reactively
|
||||
const hasNWC = useMemo(() => {
|
||||
return connections.length > 0 && connections.some(c => c.isConnected);
|
||||
}, [connections]);
|
||||
|
||||
// Determine preferred payment method
|
||||
const preferredMethod: WalletStatus['preferredMethod'] = activeNWC
|
||||
? 'nwc'
|
||||
@ -64,15 +78,27 @@ export function useWallet() {
|
||||
|
||||
const status: WalletStatus = {
|
||||
hasWebLN: !!webln,
|
||||
hasNWC: !!activeNWC,
|
||||
hasNWC,
|
||||
webln,
|
||||
activeNWC,
|
||||
isDetecting,
|
||||
preferredMethod,
|
||||
};
|
||||
|
||||
// Debug logging for wallet status changes
|
||||
useEffect(() => {
|
||||
console.debug('Wallet status updated:', {
|
||||
hasWebLN: status.hasWebLN,
|
||||
hasNWC: status.hasNWC,
|
||||
connectionsCount: connections.length,
|
||||
activeNWC: !!status.activeNWC,
|
||||
preferredMethod: status.preferredMethod
|
||||
});
|
||||
}, [status.hasWebLN, status.hasNWC, connections.length, status.activeNWC, status.preferredMethod]);
|
||||
|
||||
return {
|
||||
...status,
|
||||
hasAttemptedDetection,
|
||||
detectWebLN,
|
||||
testWebLN,
|
||||
};
|
||||
|
@ -3,12 +3,12 @@ import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { useAppContext } from '@/hooks/useAppContext';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useNWC } from '@/hooks/useNWC';
|
||||
import { useNWC } from '@/hooks/useNWCContext';
|
||||
import { useNostrPublish } from '@/hooks/useNostrPublish';
|
||||
import type { NWCConnection } from '@/hooks/useNWC';
|
||||
import { nip57, nip19 } from 'nostr-tools';
|
||||
import type { Event } from 'nostr-tools';
|
||||
import type { WebLNProvider } from 'webln';
|
||||
import { LNURL } from '@nostrify/nostrify/ln';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import type { NostrEvent, NostrFilter } from '@nostrify/nostrify';
|
||||
@ -24,30 +24,30 @@ function parseNWCUri(uri: string): NWCConnection | null {
|
||||
const walletPubkey = url.pathname.replace('//', '');
|
||||
const secret = url.searchParams.get('secret');
|
||||
const relayParam = url.searchParams.getAll('relay');
|
||||
const lud16 = url.searchParams.get('lud16') || undefined;
|
||||
const _lud16 = url.searchParams.get('lud16') || undefined;
|
||||
|
||||
if (!walletPubkey || !secret || relayParam.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
walletPubkey,
|
||||
secret,
|
||||
relayUrls: relayParam,
|
||||
lud16,
|
||||
connectionString: uri,
|
||||
alias: 'Parsed NWC',
|
||||
isConnected: false,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function useZaps(target: Event, webln: WebLNProvider | null, nwcConnection: NWCConnection | null, onZapSuccess?: () => void) {
|
||||
export function useZaps(target: Event, webln: WebLNProvider | null, _nwcConnection: NWCConnection | null, onZapSuccess?: () => void) {
|
||||
const { nostr } = useNostr();
|
||||
const { toast } = useToast();
|
||||
const { user } = useCurrentUser();
|
||||
const { config } = useAppContext();
|
||||
const { mutate: publishEvent } = useNostrPublish();
|
||||
const author = useAuthor(target?.pubkey);
|
||||
const { sendNWCRequest } = useNWC();
|
||||
const { sendPayment, getActiveConnection, connections, activeConnection } = useNWC();
|
||||
const [isZapping, setIsZapping] = useState(false);
|
||||
const [invoice, setInvoice] = useState<string | null>(null);
|
||||
|
||||
@ -104,6 +104,7 @@ export function useZaps(target: Event, webln: WebLNProvider | null, nwcConnectio
|
||||
}
|
||||
|
||||
setIsZapping(true);
|
||||
setInvoice(null); // Clear any previous invoice at the start
|
||||
|
||||
if (!user) {
|
||||
toast({
|
||||
@ -126,74 +127,166 @@ export function useZaps(target: Event, webln: WebLNProvider | null, nwcConnectio
|
||||
return;
|
||||
}
|
||||
|
||||
const { lud06, lud16 } = author.data.metadata;
|
||||
if (!lud16 && !lud06) {
|
||||
const { lud16 } = author.data.metadata;
|
||||
if (!lud16) {
|
||||
toast({
|
||||
title: 'Lightning address not found',
|
||||
description: 'The author does not have a lightning address (lud16 or lud06) configured.',
|
||||
description: 'The author does not have a lightning address configured.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
setIsZapping(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const lnurl = lud06 ? LNURL.fromString(lud06) : LNURL.fromLightningAddress(lud16!);
|
||||
const zapAmount = amount * 1000; // convert to millisats
|
||||
const zapRequest = await user.signer.signEvent(nip57.makeZapRequest({
|
||||
profile: target.pubkey,
|
||||
event: target,
|
||||
amount: zapAmount,
|
||||
relays: [config.relayUrl],
|
||||
comment: comment,
|
||||
}));
|
||||
// Get zap endpoint using the old reliable method
|
||||
const zapEndpoint = await nip57.getZapEndpoint(author.data.event as Event);
|
||||
if (!zapEndpoint) {
|
||||
toast({
|
||||
title: 'Zap endpoint not found',
|
||||
description: 'Could not find a zap endpoint for the author.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
setIsZapping(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { pr: newInvoice } = await lnurl.getInvoice({
|
||||
const zapAmount = amount * 1000; // convert to millisats
|
||||
const relays = [config.relayUrl];
|
||||
|
||||
// Create zap request (unsigned, like the old implementation)
|
||||
const zapRequest = nip57.makeZapRequest({
|
||||
profile: target.pubkey,
|
||||
event: target.id,
|
||||
amount: zapAmount,
|
||||
nostr: zapRequest,
|
||||
relays,
|
||||
comment: comment,
|
||||
});
|
||||
|
||||
// Try NWC first if available
|
||||
if (nwcConnection) {
|
||||
try {
|
||||
const response = await sendNWCRequest(nwcConnection, {
|
||||
method: 'pay_invoice',
|
||||
params: {
|
||||
invoice: newInvoice,
|
||||
amount: zapAmount,
|
||||
},
|
||||
});
|
||||
// Handle addressable events (restored from old implementation)
|
||||
if (naddr) {
|
||||
const decoded = nip19.decode(naddr).data as nip19.AddressPointer;
|
||||
zapRequest.tags.push(["a", `${decoded.kind}:${decoded.pubkey}:${decoded.identifier}`]);
|
||||
zapRequest.tags = zapRequest.tags.filter(t => t[0] !== 'e');
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(`NWC Error: ${response.error.message}`);
|
||||
// Sign and publish the zap request
|
||||
publishEvent(zapRequest, {
|
||||
onSuccess: async (event) => {
|
||||
try {
|
||||
// Use the old fetch method - more reliable than LNURL validation
|
||||
const res = await fetch(`${zapEndpoint}?amount=${zapAmount}&nostr=${encodeURI(JSON.stringify(event))}`);
|
||||
const responseData = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}: ${responseData.reason || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
const newInvoice = responseData.pr;
|
||||
if (!newInvoice || typeof newInvoice !== 'string') {
|
||||
throw new Error('Lightning service did not return a valid invoice');
|
||||
}
|
||||
|
||||
// Get the current active NWC connection dynamically
|
||||
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
|
||||
if (currentNWCConnection && currentNWCConnection.connectionString && currentNWCConnection.isConnected) {
|
||||
try {
|
||||
console.debug('Attempting NWC payment...', {
|
||||
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
|
||||
setIsZapping(false);
|
||||
setInvoice(null);
|
||||
|
||||
toast({
|
||||
title: 'Zap successful!',
|
||||
description: `You sent ${amount} sats via NWC to the author.`,
|
||||
});
|
||||
|
||||
// Close dialog last to ensure clean state
|
||||
onZapSuccess?.();
|
||||
return;
|
||||
} catch (nwcError) {
|
||||
console.error('NWC payment failed, falling back:', nwcError);
|
||||
|
||||
// Show specific NWC error to user for debugging
|
||||
const errorMessage = nwcError instanceof Error ? nwcError.message : 'Unknown NWC error';
|
||||
toast({
|
||||
title: 'NWC payment failed',
|
||||
description: `${errorMessage}. Falling back to other payment methods...`,
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to WebLN or manual payment
|
||||
if (webln) {
|
||||
await webln.sendPayment(newInvoice);
|
||||
|
||||
// Clear states immediately on success
|
||||
setIsZapping(false);
|
||||
setInvoice(null);
|
||||
|
||||
toast({
|
||||
title: 'Zap successful!',
|
||||
description: `You sent ${amount} sats to the author.`,
|
||||
});
|
||||
|
||||
// Close dialog last to ensure clean state
|
||||
onZapSuccess?.();
|
||||
} else {
|
||||
setInvoice(newInvoice);
|
||||
setIsZapping(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Zap error:', err);
|
||||
toast({
|
||||
title: 'Zap failed',
|
||||
description: (err as Error).message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setIsZapping(false);
|
||||
}
|
||||
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error('Failed to publish zap request:', err);
|
||||
toast({
|
||||
title: 'Zap successful!',
|
||||
description: `You sent ${amount} sats via NWC to the author.`,
|
||||
});
|
||||
onZapSuccess?.();
|
||||
return;
|
||||
} catch (nwcError) {
|
||||
console.error('NWC payment failed, falling back:', nwcError);
|
||||
toast({
|
||||
title: 'NWC payment failed',
|
||||
description: 'Falling back to manual payment...',
|
||||
title: 'Zap failed',
|
||||
description: 'Failed to create zap request',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to WebLN or manual payment
|
||||
if (webln) {
|
||||
await webln.sendPayment(newInvoice);
|
||||
toast({
|
||||
title: 'Zap successful!',
|
||||
description: `You sent ${amount} sats to the author.`,
|
||||
});
|
||||
onZapSuccess?.();
|
||||
} else {
|
||||
setInvoice(newInvoice);
|
||||
}
|
||||
setIsZapping(false);
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Zap error:', err);
|
||||
toast({
|
||||
@ -201,7 +294,6 @@ export function useZaps(target: Event, webln: WebLNProvider | null, nwcConnectio
|
||||
description: (err as Error).message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setIsZapping(false);
|
||||
}
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user