mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-04-19 19:01:19 +00:00
Start of individual content payment flow
This commit is contained in:
parent
b953b76785
commit
42a9d243ca
1
package-lock.json
generated
1
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -1,4 +1,3 @@
|
||||
"use client";
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useEffect } from 'react';
|
||||
|
123
src/components/bitcoinConnect/PaymentButton.js
Normal file
123
src/components/bitcoinConnect/PaymentButton.js
Normal file
@ -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 (
|
||||
<div className="flex items-center">
|
||||
{invoice ? (
|
||||
<PayButton
|
||||
invoice={invoice.paymentRequest}
|
||||
onClick={handleModalOpen}
|
||||
onPaid={handlePaymentSuccess}
|
||||
onModalClose={handleModalClose}
|
||||
title={`Pay ${amount} sats`}
|
||||
>
|
||||
Pay Now
|
||||
</PayButton>
|
||||
) : (
|
||||
<button disabled className="p-2 bg-gray-500 text-white rounded">
|
||||
Loading...
|
||||
</button>
|
||||
)}
|
||||
<span className="ml-2 text-white text-lg">{amount} sats</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentButton;
|
@ -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]);
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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 <MDDisplay source={decryptedContent} />;
|
||||
}
|
||||
if (paidResource && !decryptedContent) {
|
||||
return <p className="text-center text-xl text-red-500">This content is paid and needs to be purchased before viewing.</p>;
|
||||
}
|
||||
if (processedEvent?.content) {
|
||||
return <MDDisplay source={processedEvent.content} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const handlePaymentSuccess = (response) => {
|
||||
console.log("response in higher level", response)
|
||||
}
|
||||
|
||||
const handlePaymentError = (error) => {
|
||||
console.log("error in higher level", error)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full px-24 pt-12 mx-auto mt-4 max-tab:px-0 max-mob:px-0 max-tab:pt-2 max-mob:pt-2'>
|
||||
<div className='w-full flex flex-row justify-between max-tab:flex-col max-mob:flex-col'>
|
||||
@ -240,15 +261,10 @@ export default function Details() {
|
||||
height={194}
|
||||
className="w-[344px] h-[194px] object-cover object-top rounded-lg"
|
||||
/>
|
||||
{bitcoinConnect ? (
|
||||
<div>
|
||||
<BitcoinConnectPayButton onClick={handleZapEvent} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full flex justify-end">
|
||||
<ZapDisplay zapAmount={zapAmount} event={processedEvent} zapsLoading={zapsLoading} />
|
||||
</div>
|
||||
)}
|
||||
<div className='w-full flex flex-row justify-between'>
|
||||
{paidResource && !decryptedContent && <PaymentButton lnAddress={'bitcoinplebdev@stacker.news'} amount={processedEvent.price} onSuccess={handlePaymentSuccess} onError={handlePaymentError} />}
|
||||
<ZapDisplay zapAmount={zapAmount} event={processedEvent} zapsLoading={zapsLoading} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -257,8 +273,8 @@ export default function Details() {
|
||||
{authorView && (
|
||||
<div className='w-[75vw] mx-auto flex flex-row justify-end mt-12'>
|
||||
<div className='w-fit flex flex-row justify-between'>
|
||||
<Button onClick={() => router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
|
||||
<Button onClick={handleDelete} label="Delete" severity='danger' outlined className="w-auto m-2 mr-0" />
|
||||
<Button onClick={() => router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
|
||||
<Button onClick={handleDelete} label="Delete" severity='danger' outlined className="w-auto m-2 mr-0" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -273,13 +289,7 @@ export default function Details() {
|
||||
</div>
|
||||
)}
|
||||
<div className='w-[75vw] mx-auto mt-12 p-12 border-t-2 border-gray-300 max-tab:p-0 max-mob:p-0 max-tab:max-w-[100vw] max-mob:max-w-[100vw]'>
|
||||
{
|
||||
decryptedContent ? (
|
||||
<MDDisplay source={decryptedContent} />
|
||||
) : (
|
||||
processedEvent?.content && <MDDisplay source={processedEvent.content} />
|
||||
)
|
||||
}
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -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 = () => {
|
||||
</h2>
|
||||
<div className="flex flex-col w-1/2 mx-auto my-4 justify-between items-center">
|
||||
<h2>Connect Your Lightning Wallet</h2>
|
||||
<BitcoinConnectButton />
|
||||
{bitcoinConnect ? <BitcoinConnectButton /> : <p>Connecting...</p>}
|
||||
</div>
|
||||
<div className="flex flex-col w-1/2 mx-auto my-4 justify-between items-center">
|
||||
<h2>Subscription</h2>
|
||||
|
Loading…
x
Reference in New Issue
Block a user