From 42a9d243ca410c22eeadd28429a66ba1eb3e4293 Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Fri, 9 Aug 2024 19:00:31 -0500 Subject: [PATCH] Start of individual content payment flow --- package-lock.json | 1 + package.json | 1 + .../BitcoinConnect.js | 1 - .../bitcoinConnect/PaymentButton.js | 123 ++++++++++++++++++ src/components/zaps/ZapDisplay.js | 2 +- src/pages/api/resources/[slug].js | 21 ++- src/pages/details/[slug]/index.js | 52 +++++--- src/pages/profile.js | 15 ++- 8 files changed, 189 insertions(+), 27 deletions(-) rename src/components/{profile => bitcoinConnect}/BitcoinConnect.js (98%) create mode 100644 src/components/bitcoinConnect/PaymentButton.js diff --git a/package-lock.json b/package-lock.json index c07d41b..b34ea48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "dependencies": { "@getalby/bitcoin-connect-react": "^3.5.3", + "@getalby/lightning-tools": "^5.0.3", "@nostr-dev-kit/ndk": "^2.10.0", "@prisma/client": "^5.17.0", "@tanstack/react-query": "^5.51.21", diff --git a/package.json b/package.json index 907315d..6f1a5fd 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@getalby/bitcoin-connect-react": "^3.5.3", + "@getalby/lightning-tools": "^5.0.3", "@nostr-dev-kit/ndk": "^2.10.0", "@prisma/client": "^5.17.0", "@tanstack/react-query": "^5.51.21", diff --git a/src/components/profile/BitcoinConnect.js b/src/components/bitcoinConnect/BitcoinConnect.js similarity index 98% rename from src/components/profile/BitcoinConnect.js rename to src/components/bitcoinConnect/BitcoinConnect.js index f8fd4ca..ea192d5 100644 --- a/src/components/profile/BitcoinConnect.js +++ b/src/components/bitcoinConnect/BitcoinConnect.js @@ -1,4 +1,3 @@ -"use client"; import dynamic from 'next/dynamic'; import { useEffect } from 'react'; diff --git a/src/components/bitcoinConnect/PaymentButton.js b/src/components/bitcoinConnect/PaymentButton.js new file mode 100644 index 0000000..67b658d --- /dev/null +++ b/src/components/bitcoinConnect/PaymentButton.js @@ -0,0 +1,123 @@ +import React, { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { initializeBitcoinConnect } from './BitcoinConnect'; +import { LightningAddress } from '@getalby/lightning-tools'; +import { useToast } from '@/hooks/useToast'; + +const PayButton = dynamic( + () => import('@getalby/bitcoin-connect-react').then((mod) => mod.PayButton), + { + ssr: false, + } +); + +const PaymentButton = ({ lnAddress, amount, onSuccess, onError }) => { + const [invoice, setInvoice] = useState(null); + const { showToast } = useToast(); + const [pollingInterval, setPollingInterval] = useState(null); + + useEffect(() => { + initializeBitcoinConnect(); + }, []); + + useEffect(() => { + const fetchInvoice = async () => { + try { + const ln = new LightningAddress(lnAddress); + await ln.fetch(); + const invoice = await ln.requestInvoice({ satoshi: amount }); + setInvoice(invoice); + } catch (error) { + console.error('Error fetching invoice:', error); + showToast('error', 'Invoice Error', 'Failed to fetch the invoice.'); + if (onError) onError(error); + } + }; + + fetchInvoice(); + }, [lnAddress, amount, onError, showToast]); + + const startPolling = (invoice) => { + const intervalId = setInterval(async () => { + try { + const paid = await invoice.verifyPayment(); + console.log('Polling for payment - Paid:', paid); + if (paid) { + clearInterval(intervalId); // Stop polling + handlePaymentSuccess(invoice); + } + } catch (error) { + console.error('Polling error:', error); + clearInterval(intervalId); // Stop polling on error + handlePaymentError(error); + } + }, 5000); // Poll every 5 seconds + + setPollingInterval(intervalId); + }; + + const stopPolling = () => { + if (pollingInterval) { + clearInterval(pollingInterval); + setPollingInterval(null); + } + }; + + const handlePaymentSuccess = async (response) => { + stopPolling(); // Stop polling after success + + // Close the modal + await closeModal(); + + // After the modal is closed, show the success toast + showToast('success', 'Payment Successful', `Paid ${amount} sats`); + if (onSuccess) onSuccess(response); + }; + + const handlePaymentError = (error) => { + console.error('Payment failed:', error); + showToast('error', 'Payment Failed', error.message || 'An error occurred during payment.'); + if (onError) onError(error); + stopPolling(); // Stop polling on error + }; + + const handleModalOpen = () => { + console.log('Modal opened'); + if (invoice) { + startPolling(invoice); // Start polling when modal is opened + } + }; + + const handleModalClose = () => { + console.log('Modal closed'); + stopPolling(); // Stop polling when modal is closed + }; + + const closeModal = async () => { + const { closeModal } = await import('@getalby/bitcoin-connect-react'); + closeModal(); + }; + + return ( +
+ {invoice ? ( + + Pay Now + + ) : ( + + )} + {amount} sats +
+ ); +}; + +export default PaymentButton; diff --git a/src/components/zaps/ZapDisplay.js b/src/components/zaps/ZapDisplay.js index 5770528..f6cec51 100644 --- a/src/components/zaps/ZapDisplay.js +++ b/src/components/zaps/ZapDisplay.js @@ -11,7 +11,7 @@ const ZapDisplay = ({ zapAmount, event, zapsLoading }) => { let timeout; if (!zapsLoading && zapAmount === 0) { setExtraLoading(true); - timeout = setTimeout(() => setExtraLoading(false), 5000); + timeout = setTimeout(() => setExtraLoading(false), 3000); } return () => clearTimeout(timeout); }, [zapsLoading, zapAmount]); diff --git a/src/pages/api/resources/[slug].js b/src/pages/api/resources/[slug].js index b217adb..f272dfa 100644 --- a/src/pages/api/resources/[slug].js +++ b/src/pages/api/resources/[slug].js @@ -16,8 +16,25 @@ export default async function handler(req, res) { } } else if (req.method === 'PUT') { try { - const resource = await updateResource(slug, req.body); - res.status(200).json(resource); + // Fetch the resource by ID to check if it's part of a course + const resource = await getResourceById(slug); + + if (!resource) { + return res.status(404).json({ error: 'Resource not found' }); + } + + // Check if the resource is part of a course + const isPartOfAnyCourse = resource.courseId !== null; + + if (isPartOfAnyCourse) { + // Update the specific lesson in the course + await updateLessonInCourse(resource.courseId, slug, req.body); + } + + // Update the resource + const updatedResource = await updateResource(slug, req.body); + + res.status(200).json(updatedResource); } catch (error) { res.status(400).json({ error: error.message }); } diff --git a/src/pages/details/[slug]/index.js b/src/pages/details/[slug]/index.js index 3bf7d7c..8274fbb 100644 --- a/src/pages/details/[slug]/index.js +++ b/src/pages/details/[slug]/index.js @@ -15,6 +15,8 @@ import ZapThreadsWrapper from '@/components/ZapThreadsWrapper'; import { useToast } from '@/hooks/useToast'; import { useNDKContext } from '@/context/NDKContext'; import { useZapsSubscription } from '@/hooks/nostrQueries/zaps/useZapsSubscription'; +import { LightningAddress } from "@getalby/lightning-tools"; +import PaymentButton from '@/components/bitcoinConnect/PaymentButton'; import 'primeicons/primeicons.css'; const MDDisplay = dynamic( @@ -56,7 +58,6 @@ export default function Details() { useEffect(() => { if (session) { - console.log('session:', session); setUser(session.user); } }, [session]); @@ -80,10 +81,9 @@ export default function Details() { useEffect(() => { const decryptContent = async () => { if (user && paidResource) { - if (user.purchased.includes(processedEvent.id) || (user?.role && user?.role.subscribed)) { + if (user?.purchased?.includes(processedEvent.id) || (user?.role && user?.role.subscribed)) { // decrypt the content const decryptedContent = await nip04.decrypt(privkey, pubkey, processedEvent.content); - console.log('decryptedContent', decryptedContent); setDecryptedContent(decryptedContent); } } @@ -198,6 +198,27 @@ export default function Details() { } } + const renderContent = () => { + if (decryptedContent) { + return ; + } + if (paidResource && !decryptedContent) { + return

This content is paid and needs to be purchased before viewing.

; + } + if (processedEvent?.content) { + return ; + } + return null; + }; + + const handlePaymentSuccess = (response) => { + console.log("response in higher level", response) + } + + const handlePaymentError = (error) => { + console.log("error in higher level", error) + } + return (
@@ -240,15 +261,10 @@ export default function Details() { height={194} className="w-[344px] h-[194px] object-cover object-top rounded-lg" /> - {bitcoinConnect ? ( -
- -
- ) : ( -
- -
- )} +
+ {paidResource && !decryptedContent && } + +
)}
@@ -257,8 +273,8 @@ export default function Details() { {authorView && (
-
)} @@ -273,13 +289,7 @@ export default function Details() { )}
- { - decryptedContent ? ( - - ) : ( - processedEvent?.content && - ) - } + {renderContent()}
); diff --git a/src/pages/profile.js b/src/pages/profile.js index 6f92d5c..319f7b5 100644 --- a/src/pages/profile.js +++ b/src/pages/profile.js @@ -7,15 +7,26 @@ import { useImageProxy } from "@/hooks/useImageProxy"; import { useSession } from 'next-auth/react'; import UserContent from "@/components/profile/UserContent"; import Image from "next/image"; -import BitcoinConnectButton from "@/components/profile/BitcoinConnect"; +import BitcoinConnectButton from "@/components/bitcoinConnect/BitcoinConnect"; const Profile = () => { const [user, setUser] = useState(null); + const [bitcoinConnect, setBitcoinConnect] = useState(false); const { data: session, status } = useSession(); const { returnImageProxy } = useImageProxy(); const menu = useRef(null); + useEffect(() => { + if (typeof window === 'undefined') return; + + const bitcoinConnectConfig = window.localStorage.getItem('bc:config'); + + if (bitcoinConnectConfig) { + setBitcoinConnect(true); + } + }, []); + useEffect(() => { if (session) { setUser(session.user); @@ -74,7 +85,7 @@ const Profile = () => {

Connect Your Lightning Wallet

- + {bitcoinConnect ? :

Connecting...

}

Subscription