Added basic cron job, endpoint, and vercel setup just wrong intervals right now, also added admin and subscriptionExpiredAt into Role schema so I can show user a special message after their subscription expired

This commit is contained in:
austinkelsay 2024-09-01 12:40:25 -05:00
parent fb02ea79af
commit e69d974ad7
9 changed files with 139 additions and 21 deletions

View File

@ -1,4 +1,3 @@
// next.config.js
const removeImports = require("next-remove-imports")(); const removeImports = require("next-remove-imports")();
module.exports = removeImports({ module.exports = removeImports({
@ -9,4 +8,12 @@ module.exports = removeImports({
webpack(config, options) { webpack(config, options) {
return config; return config;
}, },
async rewrites() {
return [
{
source: '/api/cron',
destination: '/api/cron',
},
];
},
}); });

View File

@ -15,8 +15,10 @@ CREATE TABLE "Role" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"userId" TEXT NOT NULL, "userId" TEXT NOT NULL,
"subscribed" BOOLEAN NOT NULL DEFAULT false, "subscribed" BOOLEAN NOT NULL DEFAULT false,
"admin" BOOLEAN NOT NULL DEFAULT false,
"subscriptionStartDate" TIMESTAMP(3), "subscriptionStartDate" TIMESTAMP(3),
"lastPaymentAt" TIMESTAMP(3), "lastPaymentAt" TIMESTAMP(3),
"subscriptionExpiredAt" TIMESTAMP(3),
"nwc" TEXT, "nwc" TEXT,
CONSTRAINT "Role_pkey" PRIMARY KEY ("id") CONSTRAINT "Role_pkey" PRIMARY KEY ("id")

View File

@ -26,9 +26,11 @@ model Role {
id String @id @default(uuid()) id String @id @default(uuid())
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
userId String @unique userId String @unique
subscribed Boolean @default(false) subscribed Boolean @default(false)
admin Boolean @default(false)
subscriptionStartDate DateTime? subscriptionStartDate DateTime?
lastPaymentAt DateTime? lastPaymentAt DateTime?
subscriptionExpiredAt DateTime?
nwc String? nwc String?
} }

View File

@ -104,6 +104,26 @@ const SubscriptionPaymentButtons = ({ onSuccess, onError, onRecurringSubscriptio
const newNWCUrl = newNwc.getNostrWalletConnectUrl(); const newNWCUrl = newNwc.getNostrWalletConnectUrl();
if (newNWCUrl) { 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', { const subscriptionResponse = await axios.put('/api/users/subscription', {
userId: session.user.id, userId: session.user.id,
isSubscribed: true, isSubscribed: true,
@ -218,22 +238,22 @@ const SubscriptionPaymentButtons = ({ onSuccess, onError, onRecurringSubscriptio
<> <>
<Divider /> <Divider />
<div className="w-fit mx-auto flex flex-col items-center mt-24"> <div className="w-fit mx-auto flex flex-col items-center mt-24">
<AlbyButton handleSubmit={handleRecurringSubscription} /> <AlbyButton handleSubmit={handleRecurringSubscription} />
<span className='my-4 text-lg font-bold'>or</span> <span className='my-4 text-lg font-bold'>or</span>
<p className='text-lg font-bold'>Manually enter NWC URL</p> <p className='text-lg font-bold'>Manually enter NWC URL</p>
<span className='text-sm text-gray-500'>*make sure you set a budget of at least 25000 sats and set budget renewal to monthly</span> <span className='text-sm text-gray-500'>*make sure you set a budget of at least 25000 sats and set budget renewal to monthly</span>
<input <input
type="text" type="text"
value={nwcInput} value={nwcInput}
onChange={(e) => setNwcInput(e.target.value)} onChange={(e) => setNwcInput(e.target.value)}
placeholder="Enter NWC URL" placeholder="Enter NWC URL"
className="w-full p-2 mb-4 border rounded" className="w-full p-2 mb-4 border rounded"
/> />
<Button <Button
label="Submit" label="Submit"
onClick={handleManualNwcSubmit} onClick={handleManualNwcSubmit}
className="mt-4 w-fit text-[#f8f8ff]" className="mt-4 w-fit text-[#f8f8ff]"
/> />
</div> </div>
</> </>
)} )}

View File

@ -111,12 +111,14 @@ export const updateUserSubscription = async (userId, isSubscribed, nwc) => {
subscriptionStartDate: isSubscribed ? now : null, subscriptionStartDate: isSubscribed ? now : null,
lastPaymentAt: isSubscribed ? now : null, lastPaymentAt: isSubscribed ? now : null,
nwc: nwc ? nwc : null, nwc: nwc ? nwc : null,
subscriptionExpiredAt: null,
}, },
update: { update: {
subscribed: isSubscribed, subscribed: isSubscribed,
subscriptionStartDate: isSubscribed ? { set: now } : { set: null }, subscriptionStartDate: isSubscribed ? { set: now } : { set: null },
lastPaymentAt: isSubscribed ? now : { set: null }, lastPaymentAt: isSubscribed ? now : { set: null },
nwc: nwc ? nwc : 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;
};

View File

@ -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`);
}
}

View File

@ -20,12 +20,18 @@ const Profile = () => {
const [subscribeModalVisible, setSubscribeModalVisible] = useState(false); // Add this state const [subscribeModalVisible, setSubscribeModalVisible] = useState(false); // Add this state
const [subscribed, setSubscribed] = useState(false); const [subscribed, setSubscribed] = useState(false);
const [subscribedUntil, setSubscribedUntil] = useState(null); 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 { returnImageProxy } = useImageProxy();
const { ndk } = useNDKContext(); const { ndk } = useNDKContext();
const menu = useRef(null); const menu = useRef(null);
useEffect(() => {
// Refetch the session when the component mounts
update();
}, []);
useEffect(() => { useEffect(() => {
if (session && session.user) { if (session && session.user) {
setUser(session.user); setUser(session.user);
@ -35,6 +41,10 @@ const Profile = () => {
// The user is subscribed until the date in subscribedAt + 30 days // The user is subscribed until the date in subscribedAt + 30 days
const subscribedUntil = new Date(subscribedAt.getTime() + 30 * 24 * 60 * 60 * 1000); const subscribedUntil = new Date(subscribedAt.getTime() + 30 * 24 * 60 * 60 * 1000);
setSubscribedUntil(subscribedUntil); setSubscribedUntil(subscribedUntil);
if (session.user.role.subscriptionExpiredAt) {
const expiredAt = new Date(session.user.role.subscriptionExpiredAt)
setSubscriptionExpiredAt(expiredAt);
}
} }
} }
}, [session]); }, [session]);
@ -96,13 +106,14 @@ const Profile = () => {
<BitcoinConnectButton /> <BitcoinConnectButton />
</div> </div>
<div className="flex flex-col w-1/2 mx-auto my-4 justify-between items-center border-2 border-gray-700 bg-[#121212] p-8 rounded-md"> <div className="flex flex-col w-1/2 mx-auto my-4 justify-between items-center border-2 border-gray-700 bg-[#121212] p-8 rounded-md">
{subscribed ? ( {subscribed && (
<> <>
<Message severity="success" text="Subscribed!" /> <Message severity="success" text="Subscribed!" />
<p className="mt-8">Thank you for your support 🎉</p> <p className="mt-8">Thank you for your support 🎉</p>
<p className="text-sm text-gray-400">Pay-as-you-go subscription will renew on {subscribedUntil.toLocaleDateString()}</p> <p className="text-sm text-gray-400">Pay-as-you-go subscription will renew on {subscribedUntil.toLocaleDateString()}</p>
</> </>
) : ( )}
{(!subscribed && !subscriptionExpiredAt) && (
<> <>
<Message severity="info" text="You currently have no active subscription" /> <Message severity="info" text="You currently have no active subscription" />
<Button <Button
@ -112,6 +123,16 @@ const Profile = () => {
/> />
</> </>
)} )}
{subscriptionExpiredAt && (
<>
<Message severity="warn" text={`Your subscription expired on ${subscriptionExpiredAt.toLocaleDateString()}`} />
<Button
label="Subscribe"
className="w-auto mt-8 text-[#f8f8ff]"
onClick={openSubscribeModal} // Add this onClick handler
/>
</>
)}
</div> </div>
</div> </div>
{!session || !session?.user || !ndk ? ( {!session || !session?.user || !ndk ? (

9
vercel.json Normal file
View File

@ -0,0 +1,9 @@
{
"version": 2,
"crons": [
{
"path": "/api/users/subscription/cron",
"schedule": "0 * * * *"
}
]
}