plebdevs/src/components/bitcoinConnect/SubscriptionPaymentButton.js

349 lines
11 KiB
JavaScript
Raw Normal View History

import React, { useState, useEffect } from 'react';
import { track } from '@vercel/analytics';
import { initializeBitcoinConnect } from './BitcoinConnect';
import { LightningAddress } from '@getalby/lightning-tools';
import { useToast } from '@/hooks/useToast';
import { useSession } from 'next-auth/react';
2025-05-12 18:50:32 -05:00
import { webln } from '@getalby/sdk';
import { useRouter } from 'next/router';
2024-08-31 21:52:50 -05:00
import { Divider } from 'primereact/divider';
import dynamic from 'next/dynamic';
import AlbyButton from '@/components/buttons/AlbyButton';
2024-09-10 15:44:08 -05:00
import GenericButton from '@/components/buttons/GenericButton';
import axios from 'axios';
2024-08-31 21:52:50 -05:00
import Image from 'next/image';
const PaymentModal = dynamic(
2025-04-02 17:47:30 -05:00
() => import('@getalby/bitcoin-connect-react').then(mod => mod.Payment),
{ ssr: false }
);
2025-04-02 17:47:30 -05:00
const SubscriptionPaymentButtons = ({
onSuccess,
onError,
onRecurringSubscriptionSuccess,
setIsProcessing,
oneTime = false,
recurring = false,
layout = 'row',
}) => {
const [invoice, setInvoice] = useState(null);
const [showRecurringOptions, setShowRecurringOptions] = useState(false);
const [nwcInput, setNwcInput] = useState('');
2025-05-12 18:50:32 -05:00
const [bitcoinConnectClient, setBitcoinConnectClient] = useState(null);
2025-04-02 17:47:30 -05:00
const { showToast } = useToast();
const { data: session, status } = useSession();
const router = useRouter();
2025-04-02 17:47:30 -05:00
const lnAddress = process.env.NEXT_PUBLIC_LIGHTNING_ADDRESS;
const amount = 50000;
2025-04-02 17:47:30 -05:00
useEffect(() => {
2025-05-12 18:50:32 -05:00
// Initialize Bitcoin Connect as early as possible
const initBC = async () => {
try {
const client = await initializeBitcoinConnect();
console.log("Client in SubscriptionPaymentButton:", client);
setBitcoinConnectClient(client);
} catch (err) {
console.error("Error initializing Bitcoin Connect in SubscriptionPaymentButton:", err);
}
};
initBC();
2025-04-02 17:47:30 -05:00
}, []);
2025-04-02 17:47:30 -05:00
useEffect(() => {
let intervalId;
if (invoice) {
intervalId = setInterval(async () => {
const paid = await invoice.verifyPayment();
2025-04-02 17:47:30 -05:00
if (paid && invoice.preimage) {
clearInterval(intervalId);
// handle success
onSuccess();
}
2025-04-02 17:47:30 -05:00
}, 1000);
} else {
console.error('no invoice');
}
2025-04-02 17:47:30 -05:00
return () => {
if (intervalId) {
clearInterval(intervalId);
}
};
2025-04-02 17:47:30 -05:00
}, [invoice]);
2025-04-02 17:47:30 -05:00
const fetchInvoice = async () => {
try {
const ln = new LightningAddress(lnAddress);
await ln.fetch();
const newInvoice = await ln.requestInvoice({
satoshi: amount,
comment: `Subscription Purchase. User: ${session?.user?.id}`,
});
return newInvoice;
} catch (error) {
console.error('Error fetching invoice:', error);
showToast('error', 'Invoice Error', `Failed to fetch the invoice: ${error.message}`);
if (onError) onError(error);
return null;
}
};
2025-04-02 17:47:30 -05:00
const handlePaymentSuccess = async response => {
track('Subscription Payment', { method: 'pay_as_you_go', userId: session?.user?.id });
showToast('success', 'Payment Successful', 'Your payment has been processed successfully.');
if (onSuccess) onSuccess(response);
};
2025-04-02 17:47:30 -05:00
const handlePaymentError = async error => {
console.error('Payment error', error);
showToast('error', 'Payment Failed', `An error occurred during payment: ${error.message}`);
if (onError) onError(error);
};
2025-04-02 17:47:30 -05:00
const handleRecurringSubscription = async () => {
setIsProcessing(true);
2025-05-12 18:50:32 -05:00
// Re-initialize if not already initialized
if (!bitcoinConnectClient) {
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;
}
}
2025-04-02 17:47:30 -05:00
try {
2025-05-12 18:50:32 -05:00
// Import the SDK directly to avoid client issues
const { nwc } = await import('@getalby/sdk');
const newNwc = nwc.NWCClient.withNewSecret();
const yearFromNow = new Date();
yearFromNow.setFullYear(yearFromNow.getFullYear() + 1);
2025-04-02 17:47:30 -05:00
const initNwcOptions = {
name: 'plebdevs.com',
requestMethods: ['pay_invoice'],
maxAmount: 50000,
editable: false,
budgetRenewal: 'monthly',
expiresAt: yearFromNow,
};
2025-05-12 18:50:32 -05:00
// Initialize NWC directly with the SDK
2025-04-02 17:47:30 -05:00
await newNwc.initNWC(initNwcOptions);
showToast('info', 'Alby', 'Alby connection window opened.');
2025-05-12 18:50:32 -05:00
// Get NWC URL directly
2025-04-02 17:47:30 -05:00
const newNWCUrl = newNwc.getNostrWalletConnectUrl();
2025-04-02 17:47:30 -05:00
if (newNWCUrl) {
2025-05-12 18:50:32 -05:00
const nwcProvider = new webln.NostrWebLNProvider({
2025-04-02 17:47:30 -05:00
nostrWalletConnectUrl: newNWCUrl,
});
2025-05-12 18:50:32 -05:00
await nwcProvider.enable();
2025-04-02 17:47:30 -05:00
const invoice = await fetchInvoice();
2025-04-02 17:47:30 -05:00
if (!invoice || !invoice.paymentRequest) {
showToast('error', 'NWC', `Failed to fetch invoice from ${lnAddress}`);
2025-05-12 18:50:32 -05:00
setIsProcessing(false);
2025-04-02 17:47:30 -05:00
return;
}
2025-05-12 18:50:32 -05:00
const paymentResponse = await nwcProvider.sendPayment(invoice.paymentRequest);
2025-04-02 17:47:30 -05:00
if (!paymentResponse || !paymentResponse?.preimage) {
showToast('error', 'NWC', 'Payment failed');
2025-05-12 18:50:32 -05:00
setIsProcessing(false);
2025-04-02 17:47:30 -05:00
return;
}
2025-04-02 17:47:30 -05:00
const subscriptionResponse = await axios.put('/api/users/subscription', {
userId: session.user.id,
isSubscribed: true,
nwc: newNWCUrl,
});
if (subscriptionResponse.status === 200) {
track('Subscription Payment', { method: 'recurring', userId: session?.user?.id });
showToast('success', 'Subscription Setup', 'Recurring subscription setup successful!');
if (onRecurringSubscriptionSuccess) onRecurringSubscriptionSuccess();
} else {
throw new Error(`Unexpected response status: ${subscriptionResponse.status}`);
}
2025-04-02 17:47:30 -05:00
} else {
throw new Error('Failed to generate NWC URL');
}
} catch (error) {
console.error('Error initializing NWC:', error);
showToast('error', 'Subscription Setup Failed', `Error: ${error.message}`);
if (onError) onError(error);
} finally {
setIsProcessing(false);
}
};
2025-04-02 17:47:30 -05:00
const handleManualNwcSubmit = async () => {
if (!nwcInput) {
showToast('error', 'NWC', 'Please enter a valid NWC URL');
return;
}
2025-04-02 17:47:30 -05:00
setIsProcessing(true);
try {
const nwc = new webln.NostrWebLNProvider({
nostrWalletConnectUrl: nwcInput,
});
2025-04-02 17:47:30 -05:00
await nwc.enable();
2025-04-02 17:47:30 -05:00
const invoice = await fetchInvoice();
if (!invoice || !invoice.paymentRequest) {
showToast('error', 'NWC', `Failed to fetch invoice from ${lnAddress}`);
return;
}
2025-04-02 17:47:30 -05:00
const payResponse = await nwc.sendPayment(invoice.paymentRequest);
if (!payResponse || !payResponse.preimage) {
showToast('error', 'NWC', 'Payment failed');
return;
}
2025-04-02 17:47:30 -05:00
showToast('success', 'NWC', 'Payment successful!');
2025-04-02 17:47:30 -05:00
try {
const subscriptionResponse = await axios.put('/api/users/subscription', {
userId: session.user.id,
isSubscribed: true,
nwc: nwcInput,
});
if (subscriptionResponse.status === 200) {
track('Subscription Payment', { method: 'recurring-manual', userId: session?.user?.id });
showToast('success', 'NWC', 'Subscription setup successful!');
if (onRecurringSubscriptionSuccess) onRecurringSubscriptionSuccess();
} else {
throw new Error('Unexpected response status');
}
2025-04-02 17:47:30 -05:00
} catch (error) {
console.error('Subscription setup error:', error);
showToast('error', 'NWC', 'Subscription setup failed. Please contact support.');
if (onError) onError(error);
}
} catch (error) {
console.error('NWC error:', error);
showToast('error', 'NWC', `An error occurred: ${error.message}`);
if (onError) onError(error);
} finally {
setIsProcessing(false);
}
};
2025-04-02 17:47:30 -05:00
return (
<>
{!invoice && (
<div
className={`w-full flex ${layout === 'row' ? 'flex-row justify-between' : 'flex-col items-center'}`}
>
{(oneTime || (!oneTime && !recurring)) && (
<GenericButton
label="Pay as you go"
icon="pi pi-bolt"
onClick={async () => {
if (status === 'unauthenticated') {
console.log('unauthenticated');
router.push('/auth/signin');
} else {
const invoice = await fetchInvoice();
setInvoice(invoice);
}
}}
severity="primary"
className="w-fit mt-4 text-[#f8f8ff]"
/>
)}
{(recurring || (!oneTime && !recurring)) && (
<GenericButton
label="Setup Recurring Subscription"
icon={
<Image
src="/images/nwc-logo.svg"
alt="NWC Logo"
width={16}
height={16}
className="mr-2"
/>
}
severity="help"
className="w-fit mt-4 text-[#f8f8ff] bg-purple-600"
onClick={() => {
if (status === 'unauthenticated') {
console.log('unauthenticated');
router.push('/auth/signin');
} else {
setShowRecurringOptions(!showRecurringOptions);
}
}}
/>
)}
</div>
)}
{showRecurringOptions && (
<>
2025-04-02 17:47:30 -05:00
<Divider />
<div className="w-fit mx-auto flex flex-col items-center mt-24">
<AlbyButton handleSubmit={handleRecurringSubscription} />
<span className="my-4 text-lg font-bold">or</span>
<p className="text-lg font-bold">Manually enter NWC URL</p>
<span className="text-sm text-gray-500">
*make sure you set a budget of at least 50000 sats and set budget renewal to monthly
</span>
<input
type="text"
value={nwcInput}
onChange={e => setNwcInput(e.target.value)}
placeholder="Enter NWC URL"
className="w-full p-2 mb-4 border rounded"
/>
<GenericButton
label="Submit"
onClick={handleManualNwcSubmit}
className="mt-4 w-fit text-[#f8f8ff]"
/>
</div>
</>
2025-04-02 17:47:30 -05:00
)}
{invoice && invoice.paymentRequest && (
<div className="w-full mx-auto mt-8">
<PaymentModal
invoice={invoice?.paymentRequest}
onPaid={handlePaymentSuccess}
onError={handlePaymentError}
paymentMethods="external"
title={`Pay ${amount} sats`}
/>
</div>
)}
</>
);
};
2025-04-02 17:47:30 -05:00
export default SubscriptionPaymentButtons;