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")();
module.exports = removeImports({
@ -9,4 +8,12 @@ module.exports = removeImports({
webpack(config, options) {
return config;
},
async rewrites() {
return [
{
source: '/api/cron',
destination: '/api/cron',
},
];
},
});

View File

@ -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")

View File

@ -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?
}

View File

@ -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
<>
<Divider />
<div className="w-fit mx-auto flex flex-col items-center mt-24">
<AlbyButton handleSubmit={handleRecurringSubscription} />
<span className='my-4 text-lg font-bold'>or</span>
<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>
<input
type="text"
value={nwcInput}
onChange={(e) => setNwcInput(e.target.value)}
placeholder="Enter NWC URL"
className="w-full p-2 mb-4 border rounded"
/>
<Button
label="Submit"
onClick={handleManualNwcSubmit}
className="mt-4 w-fit text-[#f8f8ff]"
/>
<AlbyButton handleSubmit={handleRecurringSubscription} />
<span className='my-4 text-lg font-bold'>or</span>
<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>
<input
type="text"
value={nwcInput}
onChange={(e) => setNwcInput(e.target.value)}
placeholder="Enter NWC URL"
className="w-full p-2 mb-4 border rounded"
/>
<Button
label="Submit"
onClick={handleManualNwcSubmit}
className="mt-4 w-fit text-[#f8f8ff]"
/>
</div>
</>
)}

View File

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

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 [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 = () => {
<BitcoinConnectButton />
</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">
{subscribed ? (
{subscribed && (
<>
<Message severity="success" text="Subscribed!" />
<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>
</>
) : (
)}
{(!subscribed && !subscriptionExpiredAt) && (
<>
<Message severity="info" text="You currently have no active subscription" />
<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>
{!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 * * * *"
}
]
}