wallet configuration settings + nwc baseline support

This commit is contained in:
Chad Curtis 2025-07-13 06:47:45 +00:00
parent 55bf545646
commit 14eccbcb6f
6 changed files with 761 additions and 24 deletions

View File

@ -0,0 +1,268 @@
import { useState } from 'react';
import { Wallet, Plus, Trash2, Zap, Globe, Settings, CheckCircle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
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 { useWallet } from '@/hooks/useWallet';
import { useToast } from '@/hooks/useToast';
interface WalletModalProps {
children?: React.ReactNode;
className?: string;
}
export function WalletModal({ children, className }: WalletModalProps) {
const [open, setOpen] = useState(false);
const [addDialogOpen, setAddDialogOpen] = useState(false);
const [connectionUri, setConnectionUri] = useState('');
const [alias, setAlias] = useState('');
const [isConnecting, setIsConnecting] = useState(false);
const {
connections,
activeConnection,
connectionInfo,
addConnection,
removeConnection,
setActiveConnection
} = useNWC();
const { hasWebLN, hasNWC, isDetecting } = useWallet();
const { toast } = useToast();
const handleAddConnection = async () => {
if (!connectionUri.trim()) {
toast({
title: 'Connection URI required',
description: 'Please enter a valid NWC connection URI.',
variant: 'destructive',
});
return;
}
setIsConnecting(true);
try {
const success = await addConnection(connectionUri.trim(), alias.trim() || undefined);
if (success) {
setConnectionUri('');
setAlias('');
setAddDialogOpen(false);
}
} finally {
setIsConnecting(false);
}
};
const handleRemoveConnection = (walletPubkey: string) => {
removeConnection(walletPubkey);
};
const handleSetActive = (walletPubkey: string) => {
setActiveConnection(walletPubkey);
toast({
title: 'Active wallet changed',
description: 'The selected wallet is now active for zaps.',
});
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{children || (
<Button variant="outline" size="sm" className={className}>
<Wallet className="h-4 w-4 mr-2" />
Wallet Settings
</Button>
)}
</DialogTrigger>
<DialogContent className="sm:max-w-[500px] max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Wallet className="h-5 w-5" />
Lightning Wallet
</DialogTitle>
<DialogDescription>
Connect your lightning wallet to send zaps instantly.
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* Current Status */}
<div className="space-y-3">
<h3 className="font-medium">Current Status</h3>
<div className="grid gap-3">
{/* WebLN */}
<div className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center gap-3">
<Globe className="h-4 w-4 text-muted-foreground" />
<div>
<p className="text-sm font-medium">WebLN</p>
<p className="text-xs text-muted-foreground">Browser extension</p>
</div>
</div>
<div className="flex items-center gap-2">
{hasWebLN && <CheckCircle className="h-4 w-4 text-green-600" />}
<Badge variant={hasWebLN ? "default" : "secondary"} className="text-xs">
{isDetecting ? "..." : hasWebLN ? "Ready" : "Not Found"}
</Badge>
</div>
</div>
{/* NWC */}
<div className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center gap-3">
<Settings className="h-4 w-4 text-muted-foreground" />
<div>
<p className="text-sm font-medium">Nostr Wallet Connect</p>
<p className="text-xs text-muted-foreground">
{connections.length > 0
? `${connections.length} wallet${connections.length !== 1 ? 's' : ''} connected`
: "Remote wallet connection"
}
</p>
</div>
</div>
<div className="flex items-center gap-2">
{hasNWC && <CheckCircle className="h-4 w-4 text-green-600" />}
<Badge variant={hasNWC ? "default" : "secondary"} className="text-xs">
{hasNWC ? "Ready" : "None"}
</Badge>
</div>
</div>
</div>
</div>
<Separator />
{/* NWC Management */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-medium">Nostr Wallet Connect</h3>
<Dialog open={addDialogOpen} onOpenChange={setAddDialogOpen}>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
<Plus className="h-4 w-4 mr-1" />
Add
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Connect NWC Wallet</DialogTitle>
<DialogDescription>
Enter your connection string from a compatible wallet.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="alias">Wallet Name (optional)</Label>
<Input
id="alias"
placeholder="My Lightning Wallet"
value={alias}
onChange={(e) => setAlias(e.target.value)}
/>
</div>
<div>
<Label htmlFor="connection-uri">Connection URI</Label>
<Textarea
id="connection-uri"
placeholder="nostr+walletconnect://..."
value={connectionUri}
onChange={(e) => setConnectionUri(e.target.value)}
rows={3}
/>
</div>
</div>
<DialogFooter>
<Button
onClick={handleAddConnection}
disabled={isConnecting || !connectionUri.trim()}
>
{isConnecting ? 'Connecting...' : 'Connect'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
{/* Connected Wallets List */}
{connections.length === 0 ? (
<div className="text-center py-6 text-muted-foreground">
<p className="text-sm">No wallets connected</p>
</div>
) : (
<div className="space-y-2">
{connections.map((connection) => {
const info = connectionInfo[connection.walletPubkey];
const isActive = activeConnection === connection.walletPubkey;
return (
<div key={connection.walletPubkey} 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>
<p className="text-sm font-medium">
{connection.alias || info?.alias || 'Lightning Wallet'}
</p>
<p className="text-xs text-muted-foreground">
{connection.walletPubkey.slice(0, 16)}...
</p>
</div>
</div>
<div className="flex items-center gap-2">
{isActive && <CheckCircle className="h-4 w-4 text-green-600" />}
{!isActive && (
<Button
size="sm"
variant="ghost"
onClick={() => handleSetActive(connection.walletPubkey)}
>
<Zap className="h-3 w-3" />
</Button>
)}
<Button
size="sm"
variant="ghost"
onClick={() => handleRemoveConnection(connection.walletPubkey)}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
);
})}
</div>
)}
</div>
{/* Help */}
{!hasWebLN && connections.length === 0 && (
<>
<Separator />
<div className="text-center py-4 space-y-2">
<p className="text-sm text-muted-foreground">
Install a WebLN extension (like Alby) or connect an NWC wallet for instant zaps.
</p>
</div>
</>
)}
</div>
</DialogContent>
</Dialog>
);
}

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 } from 'lucide-react'; import { Zap, Copy, Sparkle, Sparkles, Star, Rocket, Wallet, Globe } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Dialog, Dialog,
@ -17,8 +17,7 @@ import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAuthor } from '@/hooks/useAuthor'; import { useAuthor } from '@/hooks/useAuthor';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { useZaps } from '@/hooks/useZaps'; import { useZaps } from '@/hooks/useZaps';
import type { WebLNProvider } from 'webln'; import { useWallet } from '@/hooks/useWallet';
import { requestProvider } from 'webln';
import QRCode from 'qrcode'; import QRCode from 'qrcode';
import type { Event } from 'nostr-tools'; import type { Event } from 'nostr-tools';
@ -38,11 +37,11 @@ const presetAmounts = [
export function ZapDialog({ target, children, className }: ZapDialogProps) { export function ZapDialog({ target, children, className }: ZapDialogProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [webln, setWebln] = useState<WebLNProvider | null>(null);
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 { zap, isZapping, invoice, setInvoice } = useZaps(target, webln, () => setOpen(false)); const { webln, activeNWC, hasWebLN, hasNWC, detectWebLN } = useWallet();
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>('');
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@ -56,20 +55,10 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) {
// Detect WebLN when dialog opens // Detect WebLN when dialog opens
useEffect(() => { useEffect(() => {
const detectWebLN = async () => { if (open && !hasWebLN) {
if (open && !webln) { detectWebLN();
try { }
const provider = await requestProvider(); }, [open, hasWebLN, detectWebLN]);
setWebln(provider);
} catch (error) {
console.warn('WebLN requestProvider failed:', error);
setWebln(null);
}
}
};
detectWebLN();
}, [open, webln]);
useEffect(() => { useEffect(() => {
if (invoice && qrCodeRef.current) { if (invoice && qrCodeRef.current) {
@ -136,6 +125,28 @@ 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

@ -1,7 +1,7 @@
// NOTE: This file is stable and usually should not be modified. // NOTE: This file is stable and usually should not be modified.
// It is important that all functionality in this file is preserved, and should only be modified if explicitly requested. // It is important that all functionality in this file is preserved, and should only be modified if explicitly requested.
import { ChevronDown, LogOut, UserIcon, UserPlus } from 'lucide-react'; import { ChevronDown, LogOut, UserIcon, UserPlus, Wallet } from 'lucide-react';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -11,6 +11,7 @@ import {
} from '@/components/ui/dropdown-menu.tsx'; } from '@/components/ui/dropdown-menu.tsx';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar.tsx'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar.tsx';
import { RelaySelector } from '@/components/RelaySelector'; import { RelaySelector } from '@/components/RelaySelector';
import { WalletModal } from '@/components/WalletModal';
import { useLoggedInAccounts, type Account } from '@/hooks/useLoggedInAccounts'; import { useLoggedInAccounts, type Account } from '@/hooks/useLoggedInAccounts';
import { genUserName } from '@/lib/genUserName'; import { genUserName } from '@/lib/genUserName';
@ -63,6 +64,15 @@ export function AccountSwitcher({ onAddAccountClick }: AccountSwitcherProps) {
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<WalletModal>
<DropdownMenuItem
className='flex items-center gap-2 cursor-pointer p-2 rounded-md'
onSelect={(e) => e.preventDefault()}
>
<Wallet className='w-4 h-4' />
<span>Wallet Settings</span>
</DropdownMenuItem>
</WalletModal>
<DropdownMenuItem <DropdownMenuItem
onClick={onAddAccountClick} onClick={onAddAccountClick}
className='flex items-center gap-2 cursor-pointer p-2 rounded-md' className='flex items-center gap-2 cursor-pointer p-2 rounded-md'

300
src/hooks/useNWC.ts Normal file
View File

@ -0,0 +1,300 @@
import { useState, useEffect, 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';
export interface NWCConnection {
walletPubkey: string;
secret: string;
relayUrls: string[];
lud16?: string;
alias?: string;
}
interface NWCInfo {
alias?: string;
color?: string;
pubkey?: string;
network?: string;
methods?: string[];
notifications?: string[];
}
export function useNWC() {
const { nostr } = useNostr();
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 => {
try {
const url = new URL(uri);
if (url.protocol !== 'nostr+walletconnect:') {
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 {
return null;
}
};
// Add new connection
const addConnection = async (uri: string, alias?: string): Promise<boolean> => {
const connection = parseNWCUri(uri);
if (!connection) {
toast({
title: 'Invalid NWC URI',
description: 'Please check the connection string and try again.',
variant: 'destructive',
});
return false;
}
// Check if connection already exists
const existingConnection = connections.find(c => c.walletPubkey === connection.walletPubkey);
if (existingConnection) {
toast({
title: 'Connection already exists',
description: 'This wallet is already connected.',
variant: 'destructive',
});
return false;
}
if (alias) {
connection.alias = alias;
}
try {
// Test connection by fetching info
await fetchWalletInfo(connection);
setConnections(prev => [...prev, connection]);
// Set as active if it's the first connection
if (connections.length === 0) {
setActiveConnection(connection.walletPubkey);
}
toast({
title: 'Wallet connected',
description: `Successfully connected to ${alias || 'wallet'}.`,
});
return true;
} catch {
toast({
title: 'Connection failed',
description: 'Could not connect to the wallet. Please check your connection.',
variant: 'destructive',
});
return false;
}
};
// Remove connection
const removeConnection = (walletPubkey: string) => {
setConnections(prev => prev.filter(c => c.walletPubkey !== walletPubkey));
if (activeConnection === walletPubkey) {
const remaining = connections.filter(c => c.walletPubkey !== walletPubkey);
setActiveConnection(remaining.length > 0 ? remaining[0].walletPubkey : null);
}
setConnectionInfo(prev => {
const newInfo = { ...prev };
delete newInfo[walletPubkey];
return newInfo;
});
toast({
title: 'Wallet disconnected',
description: 'The wallet connection has been removed.',
});
};
// Get active connection
const getActiveConnection = (): NWCConnection | null => {
if (!activeConnection) return null;
return connections.find(c => c.walletPubkey === activeConnection) || null;
};
// Send NWC request
const sendNWCRequest = 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');
}
// 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
try {
await nostr.event(signedRequest, {
signal: AbortSignal.timeout(10000),
relays: connection.relayUrls
});
} catch (error) {
console.warn('Failed to publish NWC request:', error);
throw new Error('Failed to publish NWC request');
}
// Listen for response
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('NWC request timeout'));
}, 30000); // 30 second timeout
// Query for response events
const checkForResponse = async () => {
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);
} catch (error) {
clearTimeout(timeout);
reject(error);
}
};
// Start checking for responses
setTimeout(checkForResponse, 1000); // Wait 1 second before first check
});
}, [nostr, user]);
// 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) });
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;
}
} catch (error) {
console.warn('Failed to fetch NWC info event:', error);
}
// 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,
activeConnection,
connectionInfo,
addConnection,
removeConnection,
setActiveConnection,
getActiveConnection,
fetchWalletInfo,
sendNWCRequest,
parseNWCUri,
};
}

79
src/hooks/useWallet.ts Normal file
View File

@ -0,0 +1,79 @@
import { useState, useEffect, useCallback } from 'react';
import { useNWC } from '@/hooks/useNWC';
import type { WebLNProvider } from 'webln';
import { requestProvider } from 'webln';
export interface WalletStatus {
hasWebLN: boolean;
hasNWC: boolean;
webln: WebLNProvider | null;
activeNWC: ReturnType<typeof useNWC>['getActiveConnection'] extends () => infer T ? T : null;
isDetecting: boolean;
preferredMethod: 'nwc' | 'webln' | 'manual';
}
export function useWallet() {
const [webln, setWebln] = useState<WebLNProvider | null>(null);
const [isDetecting, setIsDetecting] = useState(false);
const { getActiveConnection } = useNWC();
const activeNWC = getActiveConnection();
// Detect WebLN
const detectWebLN = useCallback(async () => {
if (webln || isDetecting) return webln;
setIsDetecting(true);
try {
const provider = await requestProvider();
setWebln(provider);
return provider;
} catch (error) {
console.warn('WebLN not available:', error);
setWebln(null);
return null;
} finally {
setIsDetecting(false);
}
}, [webln, isDetecting]);
// Auto-detect on mount
useEffect(() => {
detectWebLN();
}, [detectWebLN]);
// Test WebLN connection
const testWebLN = useCallback(async (): Promise<boolean> => {
if (!webln) return false;
try {
await webln.enable();
return true;
} catch (error) {
console.error('WebLN test failed:', error);
return false;
}
}, [webln]);
// Determine preferred payment method
const preferredMethod: WalletStatus['preferredMethod'] = activeNWC
? 'nwc'
: webln
? 'webln'
: 'manual';
const status: WalletStatus = {
hasWebLN: !!webln,
hasNWC: !!activeNWC,
webln,
activeNWC,
isDetecting,
preferredMethod,
};
return {
...status,
detectWebLN,
testWebLN,
};
}

View File

@ -3,21 +3,51 @@ import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAuthor } from '@/hooks/useAuthor'; import { useAuthor } from '@/hooks/useAuthor';
import { useAppContext } from '@/hooks/useAppContext'; import { useAppContext } from '@/hooks/useAppContext';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { useNWC } from '@/hooks/useNWC';
import type { NWCConnection } from '@/hooks/useNWC';
import { nip57, nip19 } from 'nostr-tools'; import { nip57, nip19 } from 'nostr-tools';
import type { Event } from 'nostr-tools'; import type { Event } from 'nostr-tools';
import type { WebLNProvider } from 'webln'; import type { WebLNProvider } from 'webln';
import { LNURL } from '@nostrify/nostrify/ln'; import { LNURL } from '@nostrify/nostrify/ln';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useNostr } from '@nostrify/react'; import { useNostr } from '@nostrify/react';
import type { NostrEvent, NostrFilter } from '@nostrify/nostrify'; import type { NostrEvent, NostrFilter } from '@nostrify/nostrify';
export function useZaps(target: Event, webln: WebLNProvider | null, onZapSuccess?: () => void) { // NWC utility functions
function parseNWCUri(uri: string): NWCConnection | null {
try {
const url = new URL(uri);
if (url.protocol !== 'nostr+walletconnect:') {
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 {
return null;
}
}
export function useZaps(target: Event, webln: WebLNProvider | null, nwcConnection: NWCConnection | null, onZapSuccess?: () => void) {
const { nostr } = useNostr(); const { nostr } = useNostr();
const { toast } = useToast(); const { toast } = useToast();
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const { config } = useAppContext(); const { config } = useAppContext();
const author = useAuthor(target?.pubkey); const author = useAuthor(target?.pubkey);
const { sendNWCRequest } = useNWC();
const [isZapping, setIsZapping] = useState(false); const [isZapping, setIsZapping] = useState(false);
const [invoice, setInvoice] = useState<string | null>(null); const [invoice, setInvoice] = useState<string | null>(null);
@ -86,7 +116,6 @@ export function useZaps(target: Event, webln: WebLNProvider | null, onZapSuccess
} }
try { try {
if (!author.data || !author.data?.metadata) { if (!author.data || !author.data?.metadata) {
toast({ toast({
title: 'Author not found', title: 'Author not found',
@ -123,6 +152,38 @@ export function useZaps(target: Event, webln: WebLNProvider | null, onZapSuccess
nostr: zapRequest, nostr: zapRequest,
}); });
// Try NWC first if available
if (nwcConnection) {
try {
const response = await sendNWCRequest(nwcConnection, {
method: 'pay_invoice',
params: {
invoice: newInvoice,
amount: zapAmount,
},
});
if (response.error) {
throw new Error(`NWC Error: ${response.error.message}`);
}
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...',
variant: 'destructive',
});
}
}
// Fallback to WebLN or manual payment
if (webln) { if (webln) {
await webln.sendPayment(newInvoice); await webln.sendPayment(newInvoice);
toast({ toast({
@ -145,5 +206,13 @@ export function useZaps(target: Event, webln: WebLNProvider | null, onZapSuccess
} }
}; };
return { zaps, ...query, zap, isZapping, invoice, setInvoice }; return {
zaps,
...query,
zap,
isZapping,
invoice,
setInvoice,
parseNWCUri,
};
} }