Payment flow for resources works

This commit is contained in:
austinkelsay 2024-08-12 17:27:47 -05:00
parent 573f560f28
commit 7808a88258
5 changed files with 95 additions and 51 deletions

View File

@ -3,6 +3,7 @@ import dynamic from 'next/dynamic';
import { initializeBitcoinConnect } from './BitcoinConnect'; import { initializeBitcoinConnect } from './BitcoinConnect';
import { LightningAddress } from '@getalby/lightning-tools'; import { LightningAddress } from '@getalby/lightning-tools';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { useSession } from 'next-auth/react';
import axios from 'axios'; // Import axios for API calls import axios from 'axios'; // Import axios for API calls
const PayButton = dynamic( 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 [invoice, setInvoice] = useState(null);
const [userId, setUserId] = useState(null);
const { showToast } = useToast(); const { showToast } = useToast();
const [pollingInterval, setPollingInterval] = useState(null); const [pollingInterval, setPollingInterval] = useState(null);
const { data: session } = useSession();
useEffect(() => {
if (session?.user) {
setUserId(session.user.id);
}
}, [session]);
useEffect(() => { useEffect(() => {
initializeBitcoinConnect(); initializeBitcoinConnect();
@ -77,7 +86,7 @@ const PaymentButton = ({ lnAddress, amount, onSuccess, onError, userId, resource
}; };
// Make an API call to add the purchase to the user // 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) { if (result.status === 200) {
showToast('success', 'Payment Successful', `Paid ${amount} sats and updated user purchases`); 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;

View File

@ -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({ return await prisma.user.update({
where: { id: userId }, where: { id: userId },
data: { data: {
purchased: { purchased: {
create: purchaseData create: {
} resourceId: purchaseData.resourceId,
amountPaid: purchaseData.amountPaid,
},
},
}, },
include: { include: {
purchased: true purchased: {
} include: {
resource: true,
},
},
},
}); });
}; };

View File

@ -20,6 +20,43 @@ const ndk = new NDK({
explicitRelayUrls: relayUrls, 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({ export default NextAuth({
providers: [ providers: [
CredentialsProvider({ CredentialsProvider({
@ -30,46 +67,19 @@ export default NextAuth({
}, },
authorize: async (credentials) => { authorize: async (credentials) => {
if (credentials?.pubkey) { if (credentials?.pubkey) {
await ndk.connect(); return await authorize(credentials.pubkey);
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 null; return null;
}, },
}), }),
], ],
callbacks: { 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 // Add combined user object to the token
if (user) { if (user) {
token.user = user; token.user = user;

View File

@ -1,11 +1,11 @@
import { addPurchaseToUser } from "@/db/models/userModels"; import { addResourcePurchaseToUser } from "@/db/models/userModels";
export default async function handler(req, res) { export default async function handler(req, res) {
if (req.method === 'POST') { if (req.method === 'POST') {
try { try {
const { userId, resourceId, amountPaid } = req.body; const { userId, resourceId, amountPaid } = req.body;
const updatedUser = await addPurchaseToUser(userId, { const updatedUser = await addResourcePurchaseToUser(userId, {
resourceId, resourceId,
amountPaid: parseInt(amountPaid, 10) // Ensure amountPaid is an integer amountPaid: parseInt(amountPaid, 10) // Ensure amountPaid is an integer
}); });

View File

@ -48,7 +48,7 @@ export default function Details() {
const [authorView, setAuthorView] = useState(false); const [authorView, setAuthorView] = useState(false);
const ndk = useNDKContext(); const ndk = useNDKContext();
const { data: session, status } = useSession(); const { data: session, update } = useSession();
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const { showToast } = useToast(); const { showToast } = useToast();
@ -81,12 +81,20 @@ export default function Details() {
useEffect(() => { useEffect(() => {
const decryptContent = async () => { const decryptContent = async () => {
if (user && paidResource) { 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 // decrypt the content
const decryptedContent = await nip04.decrypt(privkey, pubkey, processedEvent.content); const decryptedContent = await nip04.decrypt(privkey, pubkey, processedEvent.content);
setDecryptedContent(decryptedContent); setDecryptedContent(decryptedContent);
} }
} }
} }
decryptContent(); decryptContent();
}, [user, paidResource, processedEvent]); }, [user, paidResource, processedEvent]);
@ -211,12 +219,20 @@ export default function Details() {
return null; return null;
}; };
const handlePaymentSuccess = (response) => { const handlePaymentSuccess = async (response, newResource) => {
console.log("response in higher level", response) 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) => { const handlePaymentError = (error) => {
console.log("error in higher level", error) showToast('error', 'Payment Error', `Failed to purchase resource. Please try again. Error: ${error}`);
} }
return ( return (
@ -267,10 +283,12 @@ export default function Details() {
amount={processedEvent.price} amount={processedEvent.price}
onSuccess={handlePaymentSuccess} onSuccess={handlePaymentSuccess}
onError={handlePaymentError} onError={handlePaymentError}
userId={user.id} // Pass the user ID resourceId={processedEvent.d}
resourceId={processedEvent.id} // Pass the course/resource ID
/>} />}
{/* if the resource has been paid for show a green paid x sats text */}
{paidResource && decryptedContent && <p className='text-green-500'>Paid {processedEvent.price} sats</p>}
<ZapDisplay zapAmount={zapAmount} event={processedEvent} zapsLoading={zapsLoading} /> <ZapDisplay zapAmount={zapAmount} event={processedEvent} zapsLoading={zapsLoading} />
</div> </div>
</div> </div>