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