diff --git a/package-lock.json b/package-lock.json index 8eca486..a12a9ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@getalby/bitcoin-connect-react": "^3.5.3", "@getalby/lightning-tools": "^5.0.3", + "@getalby/sdk": "^3.6.1", "@nostr-dev-kit/ndk": "^2.10.0", "@nostr-dev-kit/ndk-cache-dexie": "^2.5.1", "@prisma/client": "^5.17.0", @@ -1357,28 +1358,6 @@ "@types/ms": "*" } }, - "node_modules/@types/eslint": { - "version": "8.56.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", - "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -4358,9 +4337,9 @@ "license": "MIT" }, "node_modules/enhanced-resolve": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", - "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -11757,13 +11736,12 @@ } }, "node_modules/webpack": { - "version": "5.93.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", - "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "license": "MIT", "peer": true, "dependencies": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", @@ -11772,7 +11750,7 @@ "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", diff --git a/package.json b/package.json index ae1698a..08fca98 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@getalby/bitcoin-connect-react": "^3.5.3", "@getalby/lightning-tools": "^5.0.3", + "@getalby/sdk": "^3.6.1", "@nostr-dev-kit/ndk": "^2.10.0", "@nostr-dev-kit/ndk-cache-dexie": "^2.5.1", "@prisma/client": "^5.17.0", diff --git a/src/components/bitcoinConnect/SubscriptionPaymentButton.js b/src/components/bitcoinConnect/SubscriptionPaymentButton.js index 6a84d0a..d22b8aa 100644 --- a/src/components/bitcoinConnect/SubscriptionPaymentButton.js +++ b/src/components/bitcoinConnect/SubscriptionPaymentButton.js @@ -1,21 +1,24 @@ import React, { useState, useEffect } from 'react'; import { Button } from 'primereact/button'; +import { ProgressSpinner } from 'primereact/progressspinner'; import { initializeBitcoinConnect } from './BitcoinConnect'; import { LightningAddress } from '@getalby/lightning-tools'; import { useToast } from '@/hooks/useToast'; import { useSession } from 'next-auth/react'; -import { useLocalStorageWithEffect } from '@/hooks/useLocalStroage'; +import { webln, nwc } from '@getalby/sdk'; +import { useRouter } from 'next/router'; import dynamic from 'next/dynamic'; - +import AlbyButton from '@/components/buttons/AlbyButton'; +import axios from 'axios'; const PaymentModal = dynamic( () => import('@getalby/bitcoin-connect-react').then((mod) => mod.Payment), { ssr: false } ); -const SubscriptionPaymentButtons = ({ onSuccess, onError }) => { +const SubscriptionPaymentButtons = ({ onSuccess, onError, onRecurringSubscriptionSuccess, setIsProcessing }) => { const [invoice, setInvoice] = useState(null); - const [paid, setPaid] = useState(null); - const [nwcUrl, setNwcUrl] = useState(null); + const [showRecurringOptions, setShowRecurringOptions] = useState(false); + const [nwcInput, setNwcInput] = useState(''); const { showToast } = useToast(); const { data: session } = useSession(); @@ -32,12 +35,7 @@ const SubscriptionPaymentButtons = ({ onSuccess, onError }) => { intervalId = setInterval(async () => { const paid = await invoice.verifyPayment(); - console.log('paid', paid); - if (paid && invoice.preimage) { - setPaid({ - preimage: invoice.preimage, - }); clearInterval(intervalId); // handle success onSuccess(); @@ -60,11 +58,10 @@ const SubscriptionPaymentButtons = ({ onSuccess, onError }) => { await ln.fetch(); const newInvoice = await ln.requestInvoice({ satoshi: amount }); console.log('newInvoice', newInvoice); - setInvoice(newInvoice); return newInvoice; } catch (error) { console.error('Error fetching invoice:', error); - showToast('error', 'Invoice Error', 'Failed to fetch the invoice.'); + showToast('error', 'Invoice Error', `Failed to fetch the invoice: ${error.message}`); if (onError) onError(error); return null; } @@ -73,84 +70,168 @@ const SubscriptionPaymentButtons = ({ onSuccess, onError }) => { const handlePaymentSuccess = async (response) => { console.log('Payment successful', response); clearInterval(checkPaymentInterval); + showToast('success', 'Payment Successful', 'Your payment has been processed successfully.'); + if (onSuccess) onSuccess(response); }; const handlePaymentError = async (error) => { console.error('Payment error', error); clearInterval(checkPaymentInterval); + showToast('error', 'Payment Failed', `An error occurred during payment: ${error.message}`); + if (onError) onError(error); }; const handleRecurringSubscription = async () => { - const { init, launchModal, onConnected } = await import('@getalby/bitcoin-connect-react'); - - init({ - appName: 'plebdevs.com', - filters: ['nwc'], - onConnected: async (connector) => { - console.log('connector', connector); - if (connector.type === 'nwc') { - console.log('connector inside nwc', connector); - const nwcConnector = connector; - const url = await nwcConnector.getNWCUrl(); - setNwcUrl(url); - console.log('NWC URL:', url); - // Here you can handle the NWC URL, e.g., send it to your backend + setIsProcessing(true); + const newNwc = nwc.NWCClient.withNewSecret(); + const yearFromNow = new Date(); + yearFromNow.setFullYear(yearFromNow.getFullYear() + 1); + + try { + const initNwcOptions = { + name: "plebdevs.com", + requestMethods: ['pay_invoice'], + maxAmount: 25, + editable: false, + budgetRenewal: 'monthly', + expiresAt: yearFromNow, + }; + await newNwc.initNWC(initNwcOptions); + showToast('info', 'Alby', 'Alby connection window opened.'); + const newNWCUrl = newNwc.getNostrWalletConnectUrl(); + + if (newNWCUrl) { + const subscriptionResponse = await axios.put('/api/users/subscription', { + userId: session.user.id, + isSubscribed: true, + nwc: newNWCUrl, + }); + + if (subscriptionResponse.status === 200) { + showToast('success', 'Subscription Setup', 'Recurring subscription setup successful!'); + if (onRecurringSubscriptionSuccess) onRecurringSubscriptionSuccess(); + } else { + throw new Error(`Unexpected response status: ${subscriptionResponse.status}`); } - }, - }); + } 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); + } + }; - launchModal(); + const handleManualNwcSubmit = async () => { + if (!nwcInput) { + showToast('error', 'NWC', 'Please enter a valid NWC URL'); + return; + } - // Set up a listener for the connection event - const unsubscribe = onConnected((provider) => { - console.log('Connected provider:', provider); - const nwc = provider?.client?.options?.nostrWalletConnectUrl; - // try to make payment - // if successful, encrypt and send to db with subscription object on the user - }); + setIsProcessing(true); + try { + const nwc = new webln.NostrWebLNProvider({ + nostrWalletConnectUrl: nwcInput, + }); - // Clean up the listener when the component unmounts - return () => { - unsubscribe(); - }; + await nwc.enable(); + + const invoice = await fetchInvoice(); + if (!invoice || !invoice.paymentRequest) { + showToast('error', 'NWC', `Failed to fetch invoice from ${lnAddress}`); + return; + } + + const payResponse = await nwc.sendPayment(invoice.paymentRequest); + if (!payResponse || !payResponse.preimage) { + showToast('error', 'NWC', 'Payment failed'); + return; + } + + showToast('success', 'NWC', 'Payment successful!'); + + try { + const subscriptionResponse = await axios.put('/api/users/subscription', { + userId: session.user.id, + isSubscribed: true, + nwc: nwcInput, + }); + + if (subscriptionResponse.status === 200) { + showToast('success', 'NWC', 'Subscription setup successful!'); + if (onRecurringSubscriptionSuccess) onRecurringSubscriptionSuccess(); + } else { + throw new Error('Unexpected response status'); + } + } 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); + } }; return ( <> - { - !invoice && ( -
-
- ) - } - { - - invoice && invoice.paymentRequest && ( -
- -
- ) - } + {!invoice && ( +
+
+ )} + {showRecurringOptions && ( +
+ + or +

Manually enter NWC URL

+ *make sure you set a budget of at least 25000 sats and set budget renewal to monthly + setNwcInput(e.target.value)} + placeholder="Enter NWC URL" + className="w-full p-2 mb-4 border rounded" + /> +
+ )} + {invoice && invoice.paymentRequest && ( +
+ +
+ )} ); }; diff --git a/src/components/buttons/AlbyButton.js b/src/components/buttons/AlbyButton.js new file mode 100644 index 0000000..61ebbdd --- /dev/null +++ b/src/components/buttons/AlbyButton.js @@ -0,0 +1,30 @@ +import React from 'react'; +import { Button } from 'primereact/button'; + +const AlbySVG = () => ( + + + + + + + + + + + + +); + +const AlbyButton = ({ handleSubmit }) => { + return ( + + ); +}; + +export default AlbyButton; diff --git a/src/components/profile/SubscribeModal.js b/src/components/profile/SubscribeModal.js index b9ee306..3b4c1a2 100644 --- a/src/components/profile/SubscribeModal.js +++ b/src/components/profile/SubscribeModal.js @@ -1,38 +1,59 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Dialog } from 'primereact/dialog'; -import { Button } from 'primereact/button'; +import { ProgressSpinner } from 'primereact/progressspinner'; import SubscriptionPaymentButtons from '@/components/bitcoinConnect/SubscriptionPaymentButton'; import axios from 'axios'; import { useSession } from 'next-auth/react'; import { useRouter } from 'next/router'; import { useToast } from '@/hooks/useToast'; +// todo encrypt nwc before saving in db const SubscribeModal = ({ visible, onHide }) => { const { data: session, update } = useSession(); const { showToast } = useToast(); const router = useRouter(); + const [isProcessing, setIsProcessing] = useState(false); - const handleSubscriptionSuccess = async () => { + const handleSubscriptionSuccess = async (response) => { + setIsProcessing(true); try { - const response = await axios.put('/api/users/subscription', { + const apiResponse = await axios.put('/api/users/subscription', { userId: session.user.id, isSubscribed: true, }); - if (response.data) { + if (apiResponse.data) { await update(); - showToast('success', 'Subscription successful', 'success'); + showToast('success', 'Subscription Successful', 'Your subscription has been activated.'); onHide(); - router.reload(); + } else { + throw new Error('Failed to update subscription status'); } } catch (error) { console.error('Subscription update error:', error); - showToast('error', 'Subscription failed', 'error'); + showToast('error', 'Subscription Update Failed', `Error: ${error.message}`); + } finally { + setIsProcessing(false); } }; const handleSubscriptionError = (error) => { console.error('Subscription error:', error); - showToast('error', 'Subscription failed', 'error'); + showToast('error', 'Subscription Failed', `An error occurred: ${error.message}`); + setIsProcessing(false); + }; + + const handleRecurringSubscriptionSuccess = async () => { + setIsProcessing(true); + try { + await update(); + showToast('success', 'Recurring Subscription Activated', 'Your recurring subscription has been set up successfully.'); + onHide(); + } catch (error) { + console.error('Session update error:', error); + showToast('error', 'Session Update Failed', `Error: ${error.message}`); + } finally { + setIsProcessing(false); + } }; return ( @@ -42,24 +63,35 @@ const SubscribeModal = ({ visible, onHide }) => { style={{ width: '50vw' }} onHide={onHide} > -

- Subscribe to PlebDevs and get access to: -

- -

- ALSO -

- - + {isProcessing ? ( +
+ + Processing subscription... +
+ ) : ( + <> +

+ Subscribe to PlebDevs and get access to: +

+ +

+ ALSO +

+ + + + )} ); }; diff --git a/src/db/models/userModels.js b/src/db/models/userModels.js index b7a7172..b1ba0ad 100644 --- a/src/db/models/userModels.js +++ b/src/db/models/userModels.js @@ -99,7 +99,7 @@ export const deleteUser = async (id) => { }); }; -export const updateUserSubscription = async (userId, isSubscribed) => { +export const updateUserSubscription = async (userId, isSubscribed, nwc) => { const now = new Date(); return await prisma.user.update({ where: { id: userId }, @@ -110,11 +110,13 @@ export const updateUserSubscription = async (userId, isSubscribed) => { subscribed: isSubscribed, subscriptionStartDate: isSubscribed ? now : null, lastPaymentAt: isSubscribed ? now : null, + nwc: nwc ? nwc : null, }, update: { subscribed: isSubscribed, subscriptionStartDate: isSubscribed ? { set: now } : { set: null }, lastPaymentAt: isSubscribed ? now : { set: null }, + nwc: nwc ? nwc : null, }, }, }, diff --git a/src/hooks/useLocalStroage.js b/src/hooks/useLocalStroage.js deleted file mode 100644 index 66918d1..0000000 --- a/src/hooks/useLocalStroage.js +++ /dev/null @@ -1,45 +0,0 @@ -import { useState, useEffect } from 'react'; - -// This version of the hook initializes state without immediately attempting to read from localStorage -function useLocalStorage(key, initialValue) { - const [storedValue, setStoredValue] = useState(initialValue); - - // Function to update localStorage and state - const setValue = value => { - try { - const valueToStore = value instanceof Function ? value(storedValue) : value; - setStoredValue(valueToStore); // Update state - if (typeof window !== 'undefined') { - window.localStorage.setItem(key, JSON.stringify(valueToStore)); // Update localStorage - } - } catch (error) { - console.log(error); - } - }; - - return [storedValue, setValue]; -} - -// Custom hook to handle fetching and setting data from localStorage -export function useLocalStorageWithEffect(key, initialValue) { - const [storedValue, setStoredValue] = useLocalStorage(key, initialValue); - - useEffect(() => { - if (typeof window === 'undefined') { - return; - } - try { - const item = window.localStorage.getItem(key); - // Only update if the item exists to prevent overwriting the initial value with null - if (item !== null) { - setStoredValue(JSON.parse(item)); - } - } catch (error) { - console.log(error); - } - }, [key]); // Dependencies array ensures this runs once on mount - - return [storedValue, setStoredValue]; -} - -export default useLocalStorage; \ No newline at end of file diff --git a/src/pages/api/users/subscription.js b/src/pages/api/users/subscription.js index b19c72e..d5af83c 100644 --- a/src/pages/api/users/subscription.js +++ b/src/pages/api/users/subscription.js @@ -3,9 +3,8 @@ import { updateUserSubscription } from "@/db/models/userModels"; export default async function handler(req, res) { if (req.method === 'PUT') { try { - const { userId, isSubscribed } = req.body; - - const updatedUser = await updateUserSubscription(userId, isSubscribed); + const { userId, isSubscribed, nwc } = req.body; + const updatedUser = await updateUserSubscription(userId, isSubscribed, nwc); res.status(200).json(updatedUser); } catch (error) { diff --git a/src/pages/profile.js b/src/pages/profile.js index 2acbb92..ba3a6f0 100644 --- a/src/pages/profile.js +++ b/src/pages/profile.js @@ -31,7 +31,7 @@ const Profile = () => { setUser(session.user); if (session.user.role) { setSubscribed(session.user.role.subscribed); - const subscribedAt = new Date(session.user.role.subscribedAt); + const subscribedAt = new Date(session.user.role.lastPaymentAt); // The user is subscribed until the date in subscribedAt + 30 days const subscribedUntil = new Date(subscribedAt.getTime() + 30 * 24 * 60 * 60 * 1000); setSubscribedUntil(subscribedUntil);