From 7808a88258c0c933488a64219e368a9ab9503c15 Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Mon, 12 Aug 2024 17:27:47 -0500 Subject: [PATCH] Payment flow for resources works --- .../bitcoinConnect/PaymentButton.js | 15 +++- src/db/models/userModels.js | 17 ++-- src/pages/api/auth/[...nextauth].js | 78 +++++++++++-------- .../{purchases.js => purchase/resource.js} | 4 +- src/pages/details/[slug]/index.js | 32 ++++++-- 5 files changed, 95 insertions(+), 51 deletions(-) rename src/pages/api/{purchases.js => purchase/resource.js} (78%) diff --git a/src/components/bitcoinConnect/PaymentButton.js b/src/components/bitcoinConnect/PaymentButton.js index c5a3c62..80fb11c 100644 --- a/src/components/bitcoinConnect/PaymentButton.js +++ b/src/components/bitcoinConnect/PaymentButton.js @@ -3,6 +3,7 @@ import dynamic from 'next/dynamic'; import { initializeBitcoinConnect } from './BitcoinConnect'; import { LightningAddress } from '@getalby/lightning-tools'; import { useToast } from '@/hooks/useToast'; +import { useSession } from 'next-auth/react'; import axios from 'axios'; // Import axios for API calls const PayButton = dynamic( @@ -12,10 +13,18 @@ const PayButton = dynamic( } ); -const PaymentButton = ({ lnAddress, amount, onSuccess, onError, userId, resourceId }) => { +const ResourcePaymentButton = ({ lnAddress, amount, onSuccess, onError, resourceId }) => { const [invoice, setInvoice] = useState(null); + const [userId, setUserId] = useState(null); const { showToast } = useToast(); const [pollingInterval, setPollingInterval] = useState(null); + const { data: session } = useSession(); + + useEffect(() => { + if (session?.user) { + setUserId(session.user.id); + } + }, [session]); useEffect(() => { initializeBitcoinConnect(); @@ -77,7 +86,7 @@ const PaymentButton = ({ lnAddress, amount, onSuccess, onError, userId, resource }; // Make an API call to add the purchase to the user - const result = await axios.post('/api/purchases', purchaseData); + const result = await axios.post('/api/purchase/resource', purchaseData); if (result.status === 200) { showToast('success', 'Payment Successful', `Paid ${amount} sats and updated user purchases`); @@ -138,4 +147,4 @@ const PaymentButton = ({ lnAddress, amount, onSuccess, onError, userId, resource ); }; -export default PaymentButton; +export default ResourcePaymentButton; diff --git a/src/db/models/userModels.js b/src/db/models/userModels.js index d6238c4..820ed07 100644 --- a/src/db/models/userModels.js +++ b/src/db/models/userModels.js @@ -44,17 +44,24 @@ export const getUserByPubkey = async (pubkey) => { }); } -export const addPurchaseToUser = async (userId, purchaseData) => { +export const addResourcePurchaseToUser = async (userId, purchaseData) => { return await prisma.user.update({ where: { id: userId }, data: { purchased: { - create: purchaseData - } + create: { + resourceId: purchaseData.resourceId, + amountPaid: purchaseData.amountPaid, + }, + }, }, include: { - purchased: true - } + purchased: { + include: { + resource: true, + }, + }, + }, }); }; diff --git a/src/pages/api/auth/[...nextauth].js b/src/pages/api/auth/[...nextauth].js index 8842ecd..f23a949 100644 --- a/src/pages/api/auth/[...nextauth].js +++ b/src/pages/api/auth/[...nextauth].js @@ -20,6 +20,43 @@ const ndk = new NDK({ explicitRelayUrls: relayUrls, }); +const authorize = async (pubkey) => { + await ndk.connect(); + const user = ndk.getUser({ pubkey }); + + try { + const profile = await user.fetchProfile(); + + // Check if user exists, create if not + const response = await axios.get(`${BASE_URL}/api/users/${pubkey}`); + if (response.status === 200 && response.data) { + const fields = await findKind0Fields(profile); + + // Combine user object with kind0Fields, giving priority to kind0Fields + const combinedUser = { ...fields, ...response.data }; + + // Update the user on the backend if necessary + // await axios.put(`${BASE_URL}/api/users/${combinedUser.id}`, combinedUser); + + return combinedUser; + } else if (response.status === 204) { + // Create user + if (profile) { + const fields = await findKind0Fields(profile); + console.log('FEEEEELDS', fields); + const payload = { pubkey, ...fields }; + + const createUserResponse = await axios.post(`${BASE_URL}/api/users`, payload); + return createUserResponse.data; + } + } + } catch (error) { + console.error("Nostr login error:", error); + } + return null; +} + + export default NextAuth({ providers: [ CredentialsProvider({ @@ -30,46 +67,19 @@ export default NextAuth({ }, authorize: async (credentials) => { if (credentials?.pubkey) { - await ndk.connect(); - - const user = ndk.getUser({ pubkey: credentials.pubkey }); - - try { - const profile = await user.fetchProfile(); - - // Check if user exists, create if not - const response = await axios.get(`${BASE_URL}/api/users/${credentials.pubkey}`); - if (response.status === 200 && response.data) { - const fields = await findKind0Fields(profile); - - // Combine user object with kind0Fields, giving priority to kind0Fields - const combinedUser = { ...fields, ...response.data }; - - // Update the user on the backend if necessary - // await axios.put(`${BASE_URL}/api/users/${combinedUser.id}`, combinedUser); - - return combinedUser; - } else if (response.status === 204) { - // Create user - if (profile) { - const fields = await findKind0Fields(profile); - console.log('FEEEEELDS', fields); - const payload = { pubkey: credentials.pubkey, ...fields }; - - const createUserResponse = await axios.post(`${BASE_URL}/api/users`, payload); - return createUserResponse.data; - } - } - } catch (error) { - console.error("Nostr login error:", error); - } + return await authorize(credentials.pubkey); } return null; }, }), ], callbacks: { - async jwt({ token, user }) { + async jwt({ token, trigger, user }) { + if (trigger === "update") { + // if we trigger an update call the authorize function again + const newUser = await authorize(token.user.pubkey); + token.user = newUser; + } // Add combined user object to the token if (user) { token.user = user; diff --git a/src/pages/api/purchases.js b/src/pages/api/purchase/resource.js similarity index 78% rename from src/pages/api/purchases.js rename to src/pages/api/purchase/resource.js index 62fbcac..d5d0f3f 100644 --- a/src/pages/api/purchases.js +++ b/src/pages/api/purchase/resource.js @@ -1,11 +1,11 @@ -import { addPurchaseToUser } from "@/db/models/userModels"; +import { addResourcePurchaseToUser } from "@/db/models/userModels"; export default async function handler(req, res) { if (req.method === 'POST') { try { const { userId, resourceId, amountPaid } = req.body; - const updatedUser = await addPurchaseToUser(userId, { + const updatedUser = await addResourcePurchaseToUser(userId, { resourceId, amountPaid: parseInt(amountPaid, 10) // Ensure amountPaid is an integer }); diff --git a/src/pages/details/[slug]/index.js b/src/pages/details/[slug]/index.js index c805652..443a1f9 100644 --- a/src/pages/details/[slug]/index.js +++ b/src/pages/details/[slug]/index.js @@ -48,7 +48,7 @@ export default function Details() { const [authorView, setAuthorView] = useState(false); const ndk = useNDKContext(); - const { data: session, status } = useSession(); + const { data: session, update } = useSession(); const [user, setUser] = useState(null); const { returnImageProxy } = useImageProxy(); const { showToast } = useToast(); @@ -81,12 +81,20 @@ 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?.length > 0) { + const purchasedResource = user?.purchased.find(purchase => purchase.resourceId === processedEvent.d); + if (purchasedResource) { + console.log("purchasedResource", purchasedResource) + const decryptedContent = await nip04.decrypt(privkey, pubkey, processedEvent.content); + setDecryptedContent(decryptedContent); + } + } else if (user?.role && user?.role.subscribed) { // decrypt the content const decryptedContent = await nip04.decrypt(privkey, pubkey, processedEvent.content); setDecryptedContent(decryptedContent); } } + } decryptContent(); }, [user, paidResource, processedEvent]); @@ -211,12 +219,20 @@ export default function Details() { return null; }; - const handlePaymentSuccess = (response) => { - console.log("response in higher level", response) + const handlePaymentSuccess = async (response, newResource) => { + if (response && response?.preimage) { + console.log("newResource", newResource) + // Refetch session to get the latest user data + const updated = await update(); + console.log("session after update", updated) + // router.reload(); // Optionally, reload the page if necessary + } else { + showToast('error', 'Error', 'Failed to purchase resource. Please try again.'); + } } const handlePaymentError = (error) => { - console.log("error in higher level", error) + showToast('error', 'Payment Error', `Failed to purchase resource. Please try again. Error: ${error}`); } return ( @@ -267,10 +283,12 @@ export default function Details() { amount={processedEvent.price} onSuccess={handlePaymentSuccess} onError={handlePaymentError} - userId={user.id} // Pass the user ID - resourceId={processedEvent.id} // Pass the course/resource ID + resourceId={processedEvent.d} />} + {/* if the resource has been paid for show a green paid x sats text */} + {paidResource && decryptedContent &&

Paid {processedEvent.price} sats

} +