update Bitcoin Connect integration to use client-based approach and direct SDK for NWC operations

This commit is contained in:
austinkelsay 2025-05-13 10:54:10 -05:00
parent 5dd71c3de0
commit 3ec3f69ae6
No known key found for this signature in database
GPG Key ID: 5A763922E5BA08EE
3 changed files with 75 additions and 58 deletions

View File

@ -5,14 +5,16 @@ const Button = dynamic(() => import('@getalby/bitcoin-connect-react').then(mod =
ssr: false, ssr: false,
}); });
// Module-level state
let initialized = false; let initialized = false;
let bitcoinConnectClient = null; let bitcoinConnectClient = null;
export async function initializeBitcoinConnect() { export async function initializeBitcoinConnect() {
if (!initialized) { if (!initialized) {
try { try {
// Import the full module // Import the required modules
const bc = await import('@getalby/bitcoin-connect-react'); const bc = await import('@getalby/bitcoin-connect-react');
const sdkModule = await import('@getalby/sdk');
// Initialize with the config options // Initialize with the config options
bc.init({ bc.init({
@ -23,6 +25,17 @@ export async function initializeBitcoinConnect() {
// Store the client for use in components // Store the client for use in components
bitcoinConnectClient = bc.client; bitcoinConnectClient = bc.client;
// Export NWC functionality directly
if (!bitcoinConnectClient) {
console.log('Creating backup NWC client instance');
// Create fallback if client isn't available
bitcoinConnectClient = {
nwc: sdkModule.nwc,
webln: sdkModule.webln
};
}
initialized = true; initialized = true;
console.log('Bitcoin Connect initialized successfully, client:', bitcoinConnectClient); console.log('Bitcoin Connect initialized successfully, client:', bitcoinConnectClient);
} catch (error) { } catch (error) {
@ -35,9 +48,15 @@ export async function initializeBitcoinConnect() {
} else { } else {
console.log('Bitcoin Connect already initialized'); console.log('Bitcoin Connect already initialized');
} }
return bitcoinConnectClient; return bitcoinConnectClient;
} }
// Export the SDK for direct usage
export const getSDK = async () => {
return import('@getalby/sdk');
};
const BitcoinConnectButton = () => { const BitcoinConnectButton = () => {
useEffect(() => { useEffect(() => {
initializeBitcoinConnect(); initializeBitcoinConnect();

View File

@ -1,10 +1,9 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { track } from '@vercel/analytics'; import { track } from '@vercel/analytics';
import { initializeBitcoinConnect } from './BitcoinConnect'; import { initializeBitcoinConnect, getSDK } from './BitcoinConnect';
import { LightningAddress } from '@getalby/lightning-tools'; import { LightningAddress } from '@getalby/lightning-tools';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { webln } from '@getalby/sdk';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { Divider } from 'primereact/divider'; import { Divider } from 'primereact/divider';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
@ -30,7 +29,6 @@ const SubscriptionPaymentButtons = ({
const [invoice, setInvoice] = useState(null); const [invoice, setInvoice] = useState(null);
const [showRecurringOptions, setShowRecurringOptions] = useState(false); const [showRecurringOptions, setShowRecurringOptions] = useState(false);
const [nwcInput, setNwcInput] = useState(''); const [nwcInput, setNwcInput] = useState('');
const [bitcoinConnectClient, setBitcoinConnectClient] = useState(null);
const { showToast } = useToast(); const { showToast } = useToast();
const { data: session, status } = useSession(); const { data: session, status } = useSession();
const router = useRouter(); const router = useRouter();
@ -40,33 +38,27 @@ const SubscriptionPaymentButtons = ({
useEffect(() => { useEffect(() => {
// Initialize Bitcoin Connect as early as possible // Initialize Bitcoin Connect as early as possible
const initBC = async () => { initializeBitcoinConnect().catch(err => {
try { console.error("Error initializing Bitcoin Connect:", err);
const client = await initializeBitcoinConnect(); });
console.log("Client in SubscriptionPaymentButton:", client);
setBitcoinConnectClient(client);
} catch (err) {
console.error("Error initializing Bitcoin Connect in SubscriptionPaymentButton:", err);
}
};
initBC();
}, []); }, []);
useEffect(() => { useEffect(() => {
let intervalId; let intervalId;
if (invoice) { if (invoice) {
intervalId = setInterval(async () => { intervalId = setInterval(async () => {
const paid = await invoice.verifyPayment(); try {
const paid = await invoice.verifyPayment();
if (paid && invoice.preimage) { if (paid && invoice.preimage) {
clearInterval(intervalId);
// handle success
onSuccess();
}
} catch (error) {
console.error("Error verifying payment:", error);
clearInterval(intervalId); clearInterval(intervalId);
// handle success
onSuccess();
} }
}, 1000); }, 1000);
} else {
console.error('no invoice');
} }
return () => { return () => {
@ -74,7 +66,7 @@ const SubscriptionPaymentButtons = ({
clearInterval(intervalId); clearInterval(intervalId);
} }
}; };
}, [invoice]); }, [invoice, onSuccess]);
const fetchInvoice = async () => { const fetchInvoice = async () => {
try { try {
@ -84,6 +76,7 @@ const SubscriptionPaymentButtons = ({
satoshi: amount, satoshi: amount,
comment: `Subscription Purchase. User: ${session?.user?.id}`, comment: `Subscription Purchase. User: ${session?.user?.id}`,
}); });
console.log("Invoice fetched successfully:", newInvoice);
return newInvoice; return newInvoice;
} catch (error) { } catch (error) {
console.error('Error fetching invoice:', error); console.error('Error fetching invoice:', error);
@ -106,32 +99,18 @@ const SubscriptionPaymentButtons = ({
}; };
const handleRecurringSubscription = async () => { const handleRecurringSubscription = async () => {
setIsProcessing(true); if (!setIsProcessing) {
console.warn("setIsProcessing is not defined");
// Re-initialize if not already initialized } else {
if (!bitcoinConnectClient) { setIsProcessing(true);
try {
console.log("Client not found, reinitializing");
const client = await initializeBitcoinConnect();
setBitcoinConnectClient(client);
if (!client) {
showToast('error', 'Connection Error', 'Failed to initialize Bitcoin Connect client');
setIsProcessing(false);
return;
}
} catch (err) {
console.error("Error reinitializing Bitcoin Connect:", err);
showToast('error', 'Connection Error', 'Failed to initialize Bitcoin Connect client');
setIsProcessing(false);
return;
}
} }
try { try {
// Import the SDK directly to avoid client issues // Get SDK directly to avoid client issues
const { nwc } = await import('@getalby/sdk'); const sdk = await getSDK();
const newNwc = nwc.NWCClient.withNewSecret();
// Create NWC client
const newNwc = sdk.nwc.NWCClient.withNewSecret();
const yearFromNow = new Date(); const yearFromNow = new Date();
yearFromNow.setFullYear(yearFromNow.getFullYear() + 1); yearFromNow.setFullYear(yearFromNow.getFullYear() + 1);
@ -145,36 +124,44 @@ const SubscriptionPaymentButtons = ({
expiresAt: yearFromNow, expiresAt: yearFromNow,
}; };
// Initialize NWC directly with the SDK console.log("Initializing NWC with options:", initNwcOptions);
// Initialize NWC
await newNwc.initNWC(initNwcOptions); await newNwc.initNWC(initNwcOptions);
showToast('info', 'Alby', 'Alby connection window opened.'); showToast('info', 'Alby', 'Alby connection window opened.');
// Get NWC URL directly // Get NWC URL
const newNWCUrl = newNwc.getNostrWalletConnectUrl(); const newNWCUrl = newNwc.getNostrWalletConnectUrl();
console.log("NWC URL generated:", !!newNWCUrl);
if (newNWCUrl) { if (newNWCUrl) {
const nwcProvider = new webln.NostrWebLNProvider({ const nwcProvider = new sdk.webln.NostrWebLNProvider({
nostrWalletConnectUrl: newNWCUrl, nostrWalletConnectUrl: newNWCUrl,
}); });
await nwcProvider.enable(); await nwcProvider.enable();
console.log("NWC provider enabled");
const invoice = await fetchInvoice(); const invoice = await fetchInvoice();
console.log("Invoice fetched for recurring payment:", !!invoice);
if (!invoice || !invoice.paymentRequest) { if (!invoice || !invoice.paymentRequest) {
showToast('error', 'NWC', `Failed to fetch invoice from ${lnAddress}`); showToast('error', 'NWC', `Failed to fetch invoice from ${lnAddress}`);
setIsProcessing(false); if (setIsProcessing) setIsProcessing(false);
return; return;
} }
console.log("Sending payment with NWC provider");
const paymentResponse = await nwcProvider.sendPayment(invoice.paymentRequest); const paymentResponse = await nwcProvider.sendPayment(invoice.paymentRequest);
console.log("Payment response:", paymentResponse);
if (!paymentResponse || !paymentResponse?.preimage) { if (!paymentResponse || !paymentResponse?.preimage) {
showToast('error', 'NWC', 'Payment failed'); showToast('error', 'NWC', 'Payment failed');
setIsProcessing(false); if (setIsProcessing) setIsProcessing(false);
return; return;
} }
console.log("Updating subscription in API");
const subscriptionResponse = await axios.put('/api/users/subscription', { const subscriptionResponse = await axios.put('/api/users/subscription', {
userId: session.user.id, userId: session.user.id,
isSubscribed: true, isSubscribed: true,
@ -196,7 +183,7 @@ const SubscriptionPaymentButtons = ({
showToast('error', 'Subscription Setup Failed', `Error: ${error.message}`); showToast('error', 'Subscription Setup Failed', `Error: ${error.message}`);
if (onError) onError(error); if (onError) onError(error);
} finally { } finally {
setIsProcessing(false); if (setIsProcessing) setIsProcessing(false);
} }
}; };
@ -206,29 +193,40 @@ const SubscriptionPaymentButtons = ({
return; return;
} }
setIsProcessing(true); if (setIsProcessing) setIsProcessing(true);
try { try {
const nwc = new webln.NostrWebLNProvider({ const sdk = await getSDK();
const nwc = new sdk.webln.NostrWebLNProvider({
nostrWalletConnectUrl: nwcInput, nostrWalletConnectUrl: nwcInput,
}); });
await nwc.enable(); await nwc.enable();
console.log("Manual NWC provider enabled");
const invoice = await fetchInvoice(); const invoice = await fetchInvoice();
console.log("Invoice fetched for manual NWC:", !!invoice);
if (!invoice || !invoice.paymentRequest) { if (!invoice || !invoice.paymentRequest) {
showToast('error', 'NWC', `Failed to fetch invoice from ${lnAddress}`); showToast('error', 'NWC', `Failed to fetch invoice from ${lnAddress}`);
if (setIsProcessing) setIsProcessing(false);
return; return;
} }
console.log("Sending payment with manual NWC");
const payResponse = await nwc.sendPayment(invoice.paymentRequest); const payResponse = await nwc.sendPayment(invoice.paymentRequest);
console.log("Payment response:", payResponse);
if (!payResponse || !payResponse.preimage) { if (!payResponse || !payResponse.preimage) {
showToast('error', 'NWC', 'Payment failed'); showToast('error', 'NWC', 'Payment failed');
if (setIsProcessing) setIsProcessing(false);
return; return;
} }
showToast('success', 'NWC', 'Payment successful!'); showToast('success', 'NWC', 'Payment successful!');
try { try {
console.log("Updating subscription in API (manual)");
const subscriptionResponse = await axios.put('/api/users/subscription', { const subscriptionResponse = await axios.put('/api/users/subscription', {
userId: session.user.id, userId: session.user.id,
isSubscribed: true, isSubscribed: true,
@ -252,7 +250,7 @@ const SubscriptionPaymentButtons = ({
showToast('error', 'NWC', `An error occurred: ${error.message}`); showToast('error', 'NWC', `An error occurred: ${error.message}`);
if (onError) onError(error); if (onError) onError(error);
} finally { } finally {
setIsProcessing(false); if (setIsProcessing) setIsProcessing(false);
} }
}; };

View File

@ -127,7 +127,7 @@ export default function CourseDetails({
if (session?.user && session.user?.role?.subscribed && decryptionPerformed) { if (session?.user && session.user?.role?.subscribed && decryptionPerformed) {
return ( return (
<GenericButton <GenericButton
tooltipOptions={{ position: 'top' }} tooltipOptions={{ position: 'right' }}
tooltip={`You are subscribed so you can access all paid content`} tooltip={`You are subscribed so you can access all paid content`}
icon="pi pi-check" icon="pi pi-check"
label="Subscribed" label="Subscribed"
@ -154,7 +154,7 @@ export default function CourseDetails({
outlined outlined
size="small" size="small"
tooltip={`You paid ${processedEvent.price} sats to access this course (or potentially less if a discount was applied)`} tooltip={`You paid ${processedEvent.price} sats to access this course (or potentially less if a discount was applied)`}
tooltipOptions={{ position: 'top' }} tooltipOptions={{ position: 'right' }}
className="cursor-default hover:opacity-100 hover:bg-transparent focus:ring-0" className="cursor-default hover:opacity-100 hover:bg-transparent focus:ring-0"
/> />
); );
@ -163,7 +163,7 @@ export default function CourseDetails({
if (paidCourse && author && processedEvent?.pubkey === session?.user?.pubkey) { if (paidCourse && author && processedEvent?.pubkey === session?.user?.pubkey) {
return ( return (
<GenericButton <GenericButton
tooltipOptions={{ position: 'top' }} tooltipOptions={{ position: 'right' }}
tooltip={`You created this paid course, users must pay ${processedEvent.price} sats to access it`} tooltip={`You created this paid course, users must pay ${processedEvent.price} sats to access it`}
icon="pi pi-check" icon="pi pi-check"
label={`Price ${processedEvent.price} sats`} label={`Price ${processedEvent.price} sats`}