diff --git a/next.config.js b/next.config.js index 2516072..f56648b 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,3 @@ -// next.config.js const removeImports = require("next-remove-imports")(); module.exports = removeImports({ @@ -9,4 +8,12 @@ module.exports = removeImports({ webpack(config, options) { return config; }, + async rewrites() { + return [ + { + source: '/api/cron', + destination: '/api/cron', + }, + ]; + }, }); \ No newline at end of file diff --git a/prisma/migrations/20240830221442_init/migration.sql b/prisma/migrations/20240901171426_init/migration.sql similarity index 98% rename from prisma/migrations/20240830221442_init/migration.sql rename to prisma/migrations/20240901171426_init/migration.sql index c039192..e8ee247 100644 --- a/prisma/migrations/20240830221442_init/migration.sql +++ b/prisma/migrations/20240901171426_init/migration.sql @@ -15,8 +15,10 @@ CREATE TABLE "Role" ( "id" TEXT NOT NULL, "userId" TEXT NOT NULL, "subscribed" BOOLEAN NOT NULL DEFAULT false, + "admin" BOOLEAN NOT NULL DEFAULT false, "subscriptionStartDate" TIMESTAMP(3), "lastPaymentAt" TIMESTAMP(3), + "subscriptionExpiredAt" TIMESTAMP(3), "nwc" TEXT, CONSTRAINT "Role_pkey" PRIMARY KEY ("id") diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9c5f944..6b524fe 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -26,9 +26,11 @@ model Role { id String @id @default(uuid()) user User @relation(fields: [userId], references: [id]) userId String @unique - subscribed Boolean @default(false) + subscribed Boolean @default(false) + admin Boolean @default(false) subscriptionStartDate DateTime? lastPaymentAt DateTime? + subscriptionExpiredAt DateTime? nwc String? } diff --git a/src/components/bitcoinConnect/SubscriptionPaymentButton.js b/src/components/bitcoinConnect/SubscriptionPaymentButton.js index 54ca7b8..701f2b1 100644 --- a/src/components/bitcoinConnect/SubscriptionPaymentButton.js +++ b/src/components/bitcoinConnect/SubscriptionPaymentButton.js @@ -104,6 +104,26 @@ const SubscriptionPaymentButtons = ({ onSuccess, onError, onRecurringSubscriptio const newNWCUrl = newNwc.getNostrWalletConnectUrl(); if (newNWCUrl) { + const nwc = new webln.NostrWebLNProvider({ + nostrWalletConnectUrl: newNWCUrl, + }); + + await nwc.enable(); + + const invoice = await fetchInvoice(); + + if (!invoice || !invoice.paymentRequest) { + showToast('error', 'NWC', `Failed to fetch invoice from ${lnAddress}`); + return; + } + + const paymentResponse = await nwc.sendPayment(invoice.paymentRequest); + + if (!paymentResponse || !paymentResponse?.preimage) { + showToast('error', 'NWC', 'Payment failed'); + return; + } + const subscriptionResponse = await axios.put('/api/users/subscription', { userId: session.user.id, isSubscribed: true, @@ -218,22 +238,22 @@ const SubscriptionPaymentButtons = ({ onSuccess, onError, onRecurringSubscriptio <>
- - or -

Manually enter NWC URL

- *make sure you set a budget of at least 25000 sats and set budget renewal to monthly - setNwcInput(e.target.value)} - placeholder="Enter NWC URL" - className="w-full p-2 mb-4 border rounded" - /> -
)} diff --git a/src/db/models/userModels.js b/src/db/models/userModels.js index b1ba0ad..e43e5ce 100644 --- a/src/db/models/userModels.js +++ b/src/db/models/userModels.js @@ -111,12 +111,14 @@ export const updateUserSubscription = async (userId, isSubscribed, nwc) => { subscriptionStartDate: isSubscribed ? now : null, lastPaymentAt: isSubscribed ? now : null, nwc: nwc ? nwc : null, + subscriptionExpiredAt: null, }, update: { subscribed: isSubscribed, subscriptionStartDate: isSubscribed ? { set: now } : { set: null }, lastPaymentAt: isSubscribed ? now : { set: null }, nwc: nwc ? nwc : null, + subscriptionExpiredAt: null, }, }, }, @@ -126,3 +128,38 @@ export const updateUserSubscription = async (userId, isSubscribed, nwc) => { }, }); }; + +export const checkAndUpdateExpiredSubscriptions = async () => { + const now = new Date(); + const oneHourAgo = new Date(now.getTime() - 1 * 60 * 60 * 1000); + const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000); + + const expiredSubscriptions = await prisma.role.findMany({ + where: { + subscribed: true, + lastPaymentAt: { + lt: fiveMinutesAgo + } + }, + select: { + userId: true + } + }); + + const updatePromises = expiredSubscriptions.map(({ userId }) => + prisma.role.update({ + where: { userId }, + data: { + subscribed: false, + subscriptionStartDate: null, + lastPaymentAt: null, + nwc: null, + subscriptionExpiredAt: now, + } + }) + ); + + await prisma.$transaction(updatePromises); + + return expiredSubscriptions.length; +}; diff --git a/src/pages/api/users/subscription/cron.js b/src/pages/api/users/subscription/cron.js new file mode 100644 index 0000000..6017d08 --- /dev/null +++ b/src/pages/api/users/subscription/cron.js @@ -0,0 +1,20 @@ +import { checkAndUpdateExpiredSubscriptions } from "@/db/models/userModels"; + +export default async function handler(req, res) { + if (req.headers.authorization !== `Bearer ${process.env.CRON_SECRET}`) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + if (req.method === 'POST') { + try { + const updatedCount = await checkAndUpdateExpiredSubscriptions(); + res.status(200).json({ message: `Cron job completed successfully. Updated ${updatedCount} subscriptions.` }); + } catch (error) { + console.error('Cron job error:', error); + res.status(500).json({ error: error.message }); + } + } else { + res.setHeader('Allow', ['POST']); + res.status(405).end(`Method ${req.method} Not Allowed`); + } +} diff --git a/src/pages/api/users/subscription.js b/src/pages/api/users/subscription/index.js similarity index 100% rename from src/pages/api/users/subscription.js rename to src/pages/api/users/subscription/index.js diff --git a/src/pages/profile.js b/src/pages/profile.js index 804c494..8d6e945 100644 --- a/src/pages/profile.js +++ b/src/pages/profile.js @@ -20,12 +20,18 @@ const Profile = () => { const [subscribeModalVisible, setSubscribeModalVisible] = useState(false); // Add this state const [subscribed, setSubscribed] = useState(false); const [subscribedUntil, setSubscribedUntil] = useState(null); + const [subscriptionExpiredAt, setSubscriptionExpiredAt] = useState(null); - const { data: session, status } = useSession(); + const { data: session, status, update } = useSession(); const { returnImageProxy } = useImageProxy(); const { ndk } = useNDKContext(); const menu = useRef(null); + useEffect(() => { + // Refetch the session when the component mounts + update(); + }, []); + useEffect(() => { if (session && session.user) { setUser(session.user); @@ -35,6 +41,10 @@ const Profile = () => { // The user is subscribed until the date in subscribedAt + 30 days const subscribedUntil = new Date(subscribedAt.getTime() + 30 * 24 * 60 * 60 * 1000); setSubscribedUntil(subscribedUntil); + if (session.user.role.subscriptionExpiredAt) { + const expiredAt = new Date(session.user.role.subscriptionExpiredAt) + setSubscriptionExpiredAt(expiredAt); + } } } }, [session]); @@ -96,13 +106,14 @@ const Profile = () => {
- {subscribed ? ( + {subscribed && ( <>

Thank you for your support 🎉

Pay-as-you-go subscription will renew on {subscribedUntil.toLocaleDateString()}

- ) : ( + )} + {(!subscribed && !subscriptionExpiredAt) && ( <>
{!session || !session?.user || !ndk ? ( diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..2bcda26 --- /dev/null +++ b/vercel.json @@ -0,0 +1,9 @@ +{ + "version": 2, + "crons": [ + { + "path": "/api/users/subscription/cron", + "schedule": "0 * * * *" + } + ] +}