(null);
@@ -56,20 +55,10 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) {
// Detect WebLN when dialog opens
useEffect(() => {
- const detectWebLN = async () => {
- if (open && !webln) {
- try {
- const provider = await requestProvider();
- setWebln(provider);
- } catch (error) {
- console.warn('WebLN requestProvider failed:', error);
- setWebln(null);
- }
- }
- };
-
- detectWebLN();
- }, [open, webln]);
+ if (open && !hasWebLN) {
+ detectWebLN();
+ }
+ }, [open, hasWebLN, detectWebLN]);
useEffect(() => {
if (invoice && qrCodeRef.current) {
@@ -136,6 +125,28 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) {
) : (
<>
+ {/* Payment Method Indicator */}
+
+
+ {hasNWC ? (
+ <>
+
+ Wallet Connected
+ >
+ ) : hasWebLN ? (
+ <>
+
+ WebLN Available
+ >
+ ) : (
+ <>
+
+ Manual Payment
+ >
+ )}
+
+
+
))}
+
+ e.preventDefault()}
+ >
+
+ Wallet Settings
+
+
('nwc-connections', []);
+ const [activeConnection, setActiveConnection] = useLocalStorage('nwc-active-connection', null);
+ const [connectionInfo, setConnectionInfo] = useState>({});
+
+ // 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 => {
+ 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 }
+ ): 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 => {
+ // 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,
+ };
+}
\ No newline at end of file
diff --git a/src/hooks/useWallet.ts b/src/hooks/useWallet.ts
new file mode 100644
index 0000000..ceedcfe
--- /dev/null
+++ b/src/hooks/useWallet.ts
@@ -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['getActiveConnection'] extends () => infer T ? T : null;
+ isDetecting: boolean;
+ preferredMethod: 'nwc' | 'webln' | 'manual';
+}
+
+export function useWallet() {
+ const [webln, setWebln] = useState(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 => {
+ 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,
+ };
+}
\ No newline at end of file
diff --git a/src/hooks/useZaps.ts b/src/hooks/useZaps.ts
index c6abc5f..af24b08 100644
--- a/src/hooks/useZaps.ts
+++ b/src/hooks/useZaps.ts
@@ -3,21 +3,51 @@ 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 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';
-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 { toast } = useToast();
const { user } = useCurrentUser();
const { config } = useAppContext();
const author = useAuthor(target?.pubkey);
+ const { sendNWCRequest } = useNWC();
const [isZapping, setIsZapping] = useState(false);
const [invoice, setInvoice] = useState(null);
@@ -86,7 +116,6 @@ export function useZaps(target: Event, webln: WebLNProvider | null, onZapSuccess
}
try {
-
if (!author.data || !author.data?.metadata) {
toast({
title: 'Author not found',
@@ -123,6 +152,38 @@ export function useZaps(target: Event, webln: WebLNProvider | null, onZapSuccess
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) {
await webln.sendPayment(newInvoice);
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,
+ };
}