- *make sure you set a budget of at least 50000 sats and set budget renewal to monthly
+ *make sure you set a budget of at least {(amount).toLocaleString()} sats and set budget renewal to {subscriptionType}
- Pay-as-you-go subscription will renew on {subscribedUntil.toLocaleDateString()}
+ Pay-as-you-go {user?.role?.subscriptionType || 'monthly'} subscription will renew on {subscribedUntil?.toLocaleDateString()}
- Recurring subscription will AUTO renew on {subscribedUntil.toLocaleDateString()}
+ Recurring {user?.role?.subscriptionType || 'monthly'} subscription will AUTO renew on {subscribedUntil?.toLocaleDateString()}
- Think of the subscriptions as a paetreon type model. You pay a monthly fee and in
+ Think of the subscriptions as a paetreon type model. You pay a monthly or yearly fee and in
return you get access to premium features and all of the paid content. You can
cancel at any time.
+
+
What's the difference between monthly and yearly?
+
+ The yearly subscription offers a ~17% discount compared to paying monthly for a year.
+ Both plans give you the same access to all features and content.
+
+
How do I Subscribe? (Pay as you go)
The pay as you go subscription is a one-time payment that gives you access to all
- of the premium features for one month. You will need to manually renew your
- subscription every month.
+ of the premium features for one month or year, depending on your selected plan. You will need to manually renew your
+ subscription when it expires.
How do I Subscribe? (Recurring)
The recurring subscription option allows you to submit a Nostr Wallet Connect URI
- that will be used to automatically send the subscription fee every month. You can
+ that will be used to automatically send the subscription fee on your chosen schedule. You can
cancel at any time.
diff --git a/src/config/appConfig.js b/src/config/appConfig.js
index a8325ba..092e7da 100644
--- a/src/config/appConfig.js
+++ b/src/config/appConfig.js
@@ -11,7 +11,8 @@ const appConfig = {
],
authorPubkeys: [
'f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741',
- 'c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345'
+ 'c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345',
+ '6260f29fa75c91aaa292f082e5e87b438d2ab4fdf96af398567b01802ee2fcd4',
],
customLightningAddresses: [
{
diff --git a/src/db/models/roleModels.js b/src/db/models/roleModels.js
index 51ed659..f8e011b 100644
--- a/src/db/models/roleModels.js
+++ b/src/db/models/roleModels.js
@@ -6,6 +6,7 @@ export const createRole = async data => {
user: { connect: { id: data.userId } },
admin: data.admin,
subscribed: data.subscribed,
+ subscriptionType: data.subscriptionType || 'monthly',
// Add other fields as needed, with default values or null if not provided
subscriptionStartDate: null,
lastPaymentAt: null,
diff --git a/src/db/models/userModels.js b/src/db/models/userModels.js
index 61a0952..dd97c4e 100644
--- a/src/db/models/userModels.js
+++ b/src/db/models/userModels.js
@@ -165,7 +165,7 @@ export const deleteUser = async id => {
});
};
-export const updateUserSubscription = async (userId, isSubscribed, nwc) => {
+export const updateUserSubscription = async (userId, isSubscribed, nwc, subscriptionType = 'monthly') => {
try {
const now = new Date();
return await prisma.user.update({
@@ -175,6 +175,7 @@ export const updateUserSubscription = async (userId, isSubscribed, nwc) => {
upsert: {
create: {
subscribed: isSubscribed,
+ subscriptionType: subscriptionType,
subscriptionStartDate: isSubscribed ? now : null,
lastPaymentAt: isSubscribed ? now : null,
nwc: nwc ? nwc : null,
@@ -182,6 +183,7 @@ export const updateUserSubscription = async (userId, isSubscribed, nwc) => {
},
update: {
subscribed: isSubscribed,
+ subscriptionType: subscriptionType,
subscriptionStartDate: isSubscribed ? { set: now } : { set: null },
lastPaymentAt: isSubscribed ? now : { set: null },
nwc: nwc ? nwc : null,
@@ -202,20 +204,34 @@ export const updateUserSubscription = async (userId, isSubscribed, nwc) => {
export const findExpiredSubscriptions = async () => {
try {
const now = new Date();
- const oneMonthAndOneHourAgo = new Date(
- now.getTime() - 1 * 30 * 24 * 60 * 60 * 1000 - 1 * 60 * 60 * 1000
+
+ // Define expiration periods
+ const monthlyExpiration = new Date(
+ now.getTime() - 30 * 24 * 60 * 60 * 1000 - 1 * 60 * 60 * 1000
+ );
+ const yearlyExpiration = new Date(
+ now.getTime() - 365 * 24 * 60 * 60 * 1000 - 1 * 60 * 60 * 1000
);
+ // Find expired subscriptions of both types
const result = await prisma.role.findMany({
where: {
subscribed: true,
- lastPaymentAt: {
- lt: oneMonthAndOneHourAgo,
- },
+ OR: [
+ {
+ subscriptionType: 'monthly',
+ lastPaymentAt: { lt: monthlyExpiration }
+ },
+ {
+ subscriptionType: 'yearly',
+ lastPaymentAt: { lt: yearlyExpiration }
+ }
+ ]
},
select: {
userId: true,
nwc: true,
+ subscriptionType: true,
subscriptionExpiredAt: true,
subscriptionStartDate: true,
admin: true,
@@ -231,6 +247,24 @@ export const findExpiredSubscriptions = async () => {
export const expireUserSubscriptions = async userIds => {
try {
const now = new Date();
+
+ // First, get the subscription types for each userId
+ const subscriptions = await prisma.role.findMany({
+ where: {
+ userId: { in: userIds },
+ },
+ select: {
+ userId: true,
+ subscriptionType: true,
+ },
+ });
+
+ // Create a map of userId to subscription type
+ const subscriptionTypes = {};
+ subscriptions.forEach(sub => {
+ subscriptionTypes[sub.userId] = sub.subscriptionType || 'monthly';
+ });
+
const updatePromises = userIds.map(userId =>
prisma.role.update({
where: { userId },
@@ -240,6 +274,8 @@ export const expireUserSubscriptions = async userIds => {
lastPaymentAt: null,
nwc: null,
subscriptionExpiredAt: now,
+ // Keep the subscription type for historical data and easy renewal
+ // subscriptionType: Don't change the existing value
},
})
);
diff --git a/src/pages/api/users/subscription/cron.js b/src/pages/api/users/subscription/cron.js
index 1b7012d..ebf9d9b 100644
--- a/src/pages/api/users/subscription/cron.js
+++ b/src/pages/api/users/subscription/cron.js
@@ -7,18 +7,39 @@ import { webln } from '@getalby/sdk';
import { LightningAddress } from '@getalby/lightning-tools';
const lnAddress = process.env.LIGHTNING_ADDRESS;
-const amount = 50000; // Set the subscription amount in satoshis
+
+// Calculate subscription amount based on type
+const getAmount = (subscriptionType) => {
+ // 500K for yearly (saves ~17% compared to monthly), 50K for monthly
+ return subscriptionType === 'yearly' ? 500 : 50;
+};
export default async function handler(req, res) {
if (req.method === 'GET') {
try {
+ // Get all expired subscriptions (both monthly and yearly)
+ // The findExpiredSubscriptions function handles different expiration periods:
+ // - Monthly: 30 days + 1 hour
+ // - Yearly: 365 days + 1 hour
const expiredSubscriptions = await findExpiredSubscriptions();
- console.log('expiredSubscriptions', expiredSubscriptions);
+ console.log(`Found ${expiredSubscriptions.length} expired subscriptions to process`);
+
+ // Track stats for reporting
+ const stats = {
+ monthly: { processed: 0, renewed: 0, expired: 0 },
+ yearly: { processed: 0, renewed: 0, expired: 0 },
+ };
+
const stillExpired = [];
- for (const { userId, nwc } of expiredSubscriptions) {
+ for (const { userId, nwc, subscriptionType = 'monthly' } of expiredSubscriptions) {
+ // Track processed subscriptions by type
+ stats[subscriptionType].processed++;
+
if (nwc) {
try {
+ console.log(`Processing ${subscriptionType} subscription renewal for user ${userId}`);
+ const amount = getAmount(subscriptionType);
const nwcProvider = new webln.NostrWebLNProvider({
nostrWalletConnectUrl: nwc,
});
@@ -26,30 +47,49 @@ export default async function handler(req, res) {
const ln = new LightningAddress(lnAddress);
await ln.fetch();
- const newInvoice = await ln.requestInvoice({ satoshi: amount });
+ const newInvoice = await ln.requestInvoice({
+ satoshi: amount,
+ comment: `${subscriptionType.charAt(0).toUpperCase() + subscriptionType.slice(1)} Subscription Renewal for User: ${userId}`,
+ });
+ console.log(`Generated invoice for ${amount} sats for ${subscriptionType} subscription`);
const response = await nwcProvider.sendPayment(newInvoice?.paymentRequest);
if (response && response?.preimage) {
- console.log(`SUBSCRIPTION AUTO-RENEWED`, response);
- await updateUserSubscription(userId, true, nwc);
+ console.log(`SUBSCRIPTION AUTO-RENEWED (${subscriptionType}) for User: ${userId}`);
+ // Re-subscribe the user with the same subscription type
+ await updateUserSubscription(userId, true, nwc, subscriptionType);
+ // Track successful renewals
+ stats[subscriptionType].renewed++;
continue; // Skip adding to stillExpired list
} else {
- console.log(`Payment failed for user ${userId}: (stillExpired)`, response);
+ console.log(`Payment failed for ${subscriptionType} subscription for user ${userId}: (stillExpired)`, response);
}
} catch (error) {
- console.error(`Payment failed for user ${userId}:`, error);
+ console.error(`Payment failed for ${subscriptionType} subscription for user ${userId}:`, error);
}
+ } else {
+ console.log(`No NWC found for user ${userId}, marking as expired`);
}
+
+ // Track failed renewals that will be expired
+ stats[subscriptionType].expired++;
stillExpired.push(userId);
}
+ // Expire all subscriptions that couldn't be renewed
const expiredCount = await expireUserSubscriptions(stillExpired);
+ console.log(`Processed ${expiredSubscriptions.length} total subscriptions (${stats.monthly.processed} monthly, ${stats.yearly.processed} yearly)`);
+ console.log(`Renewed ${stats.monthly.renewed + stats.yearly.renewed} total subscriptions (${stats.monthly.renewed} monthly, ${stats.yearly.renewed} yearly)`);
+ console.log(`Expired ${expiredCount} total subscriptions (${stats.monthly.expired} monthly, ${stats.yearly.expired} yearly)`);
+
res.status(200).json({
message: `Cron job completed successfully.
- Processed ${expiredSubscriptions.length} subscriptions.
- Expired ${expiredCount} subscriptions.`,
+ Processed ${expiredSubscriptions.length} subscriptions (${stats.monthly.processed} monthly, ${stats.yearly.processed} yearly).
+ Renewed ${stats.monthly.renewed + stats.yearly.renewed} subscriptions (${stats.monthly.renewed} monthly, ${stats.yearly.renewed} yearly).
+ Expired ${expiredCount} subscriptions (${stats.monthly.expired} monthly, ${stats.yearly.expired} yearly).`,
+ stats
});
} catch (error) {
console.error('Cron job error:', error);
diff --git a/src/pages/api/users/subscription/index.js b/src/pages/api/users/subscription/index.js
index 8b828b6..a06e5c5 100644
--- a/src/pages/api/users/subscription/index.js
+++ b/src/pages/api/users/subscription/index.js
@@ -12,8 +12,8 @@ export default async function handler(req, res) {
if (req.method === 'PUT') {
try {
- const { userId, isSubscribed, nwc } = req.body;
- const updatedUser = await updateUserSubscription(userId, isSubscribed, nwc);
+ const { userId, isSubscribed, nwc, subscriptionType = 'monthly' } = req.body;
+ const updatedUser = await updateUserSubscription(userId, isSubscribed, nwc, subscriptionType);
res.status(200).json(updatedUser);
} catch (error) {
diff --git a/yearly_subscriptions.md b/yearly_subscriptions.md
new file mode 100644
index 0000000..1b073be
--- /dev/null
+++ b/yearly_subscriptions.md
@@ -0,0 +1,195 @@
+# Yearly Subscription Implementation Plan
+
+## 1. Database Schema Updates
+
+```prisma
+model Role {
+ // Existing fields...
+ subscriptionType String @default("monthly") // Options: "monthly", "yearly"
+ // Other fields remain the same
+}
+```
+
+## 2. UI Component Updates
+
+### SubscribeModal.js
+- Add toggle between monthly/yearly subscription options
+- Update pricing display (50,000 sats monthly / 500,000 sats yearly)
+- Show savings message for yearly option (~17% discount)
+
+### SubscriptionPaymentButton.js
+- Add subscription type parameter
+- Modify amount calculation based on subscription type
+- Update NWC configuration for yearly budgets
+
+```javascript
+// Example modification
+const getAmount = (subscriptionType) => {
+ return subscriptionType === 'yearly' ? 500000 : 50000;
+};
+
+// For NWC setup
+const budgetRenewal = subscriptionType === 'yearly' ? 'yearly' : 'monthly';
+```
+
+## 3. API Endpoints Updates
+
+### /api/users/subscription
+- Update to accept subscriptionType parameter
+- Modify database update to store subscription type
+
+```javascript
+// Example modification
+export const updateUserSubscription = async (userId, isSubscribed, nwc, subscriptionType = 'monthly') => {
+ try {
+ const now = new Date();
+ return await prisma.user.update({
+ where: { id: userId },
+ data: {
+ role: {
+ upsert: {
+ create: {
+ subscribed: isSubscribed,
+ subscriptionType: subscriptionType,
+ // Other fields remain the same
+ },
+ update: {
+ subscribed: isSubscribed,
+ subscriptionType: subscriptionType,
+ // Other fields remain the same
+ },
+ },
+ },
+ },
+ include: {
+ role: true,
+ },
+ });
+ } finally {
+ await prisma.$disconnect();
+ }
+};
+```
+
+## 4. Cron Job Modifications
+
+### cron.js
+- Update expiration calculation to check subscription type
+- For monthly: expire after 30 days + 1 hour
+- For yearly: expire after 365 days + 1 hour
+
+```javascript
+export const findExpiredSubscriptions = async () => {
+ const now = new Date();
+
+ // Define expiration periods
+ const monthlyExpiration = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000 - 1 * 60 * 60 * 1000);
+ const yearlyExpiration = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000 - 1 * 60 * 60 * 1000);
+
+ // Find expired subscriptions of both types
+ const expiredSubscriptions = await prisma.role.findMany({
+ where: {
+ subscribed: true,
+ OR: [
+ {
+ subscriptionType: 'monthly',
+ lastPaymentAt: { lt: monthlyExpiration }
+ },
+ {
+ subscriptionType: 'yearly',
+ lastPaymentAt: { lt: yearlyExpiration }
+ }
+ ]
+ },
+ select: {
+ userId: true,
+ nwc: true,
+ subscriptionType: true,
+ subscriptionExpiredAt: true,
+ subscriptionStartDate: true,
+ admin: true,
+ }
+ });
+
+ return expiredSubscriptions;
+};
+```
+
+## 5. Testing Plan
+
+### Database Testing
+1. Verify Prisma schema correctly includes the subscriptionType field with default value "monthly"
+2. Confirm migrations apply correctly to existing database
+
+### UI Testing
+1. **Monthly Subscription UI**
+ - Verify the subscription selector defaults to monthly
+ - Check pricing shows 50,000 sats for monthly
+ - Ensure subscription buttons show "Monthly" where appropriate
+
+2. **Yearly Subscription UI**
+ - Verify selecting yearly plan updates all UI elements
+ - Check pricing shows 500,000 sats for yearly
+ - Confirm ~17% savings message appears
+ - Ensure subscription buttons show "Yearly" where appropriate
+
+### Payment Flow Testing
+1. **Monthly One-time Payment**
+ - Test subscription purchase with "Pay as you go" for monthly plan
+ - Verify subscription is created with type "monthly"
+ - Confirm user profile shows correct subscription expiration date (30 days)
+
+2. **Monthly Recurring Payment**
+ - Test subscription setup with "Setup Recurring Monthly Subscription"
+ - Verify NWC configuration with monthly budget renewal
+ - Confirm subscription is created with type "monthly"
+
+3. **Yearly One-time Payment**
+ - Test subscription purchase with "Pay as you go" for yearly plan
+ - Verify subscription is created with type "yearly"
+ - Confirm user profile shows correct subscription expiration date (365 days)
+
+4. **Yearly Recurring Payment**
+ - Test subscription setup with "Setup Recurring Yearly Subscription"
+ - Verify NWC configuration with yearly budget renewal
+ - Confirm subscription is created with type "yearly"
+
+### Cron Job Testing
+1. **Recently Active Monthly Subscription**
+ - Set up test account with monthly subscription
+ - Verify subscription not marked as expired by cron job
+
+2. **Recently Active Yearly Subscription**
+ - Set up test account with yearly subscription
+ - Verify subscription not marked as expired by cron job
+
+3. **Expired Monthly Subscription**
+ - Create test account with monthly subscription
+ - Manually adjust lastPaymentAt date to be >30 days ago
+ - Run cron job and verify subscription is expired
+
+4. **Expired Yearly Subscription**
+ - Create test account with yearly subscription
+ - Manually adjust lastPaymentAt date to be >365 days ago
+ - Run cron job and verify subscription is expired
+
+5. **Auto-renewal Testing**
+ - Set up NWC for test accounts (both monthly and yearly)
+ - Manually adjust lastPaymentAt date to trigger expiration
+ - Run cron job and verify proper renewal amount is charged
+ - Confirm subscription type is maintained after renewal
+
+## 6. Implementation Steps
+
+1. ✅ Create database migration for schema changes
+2. ✅ Modify frontend subscription components
+3. ✅ Update backend models and API endpoints
+4. ✅ Update cron job logic
+5. Test all flows thoroughly
+6. Deploy changes
+
+## 7. Marketing Considerations
+
+- Highlight savings with yearly subscription (~17% discount)
+- Update documentation and marketing materials
+- Consider grandfathering existing subscribers or offering upgrade path
From 5d884cf2b666337bb1fe4aab1690bae146bf1e68 Mon Sep 17 00:00:00 2001
From: austinkelsay
Date: Wed, 14 May 2025 14:43:01 -0500
Subject: [PATCH 2/7] update and unify modals into generic component
---
src/components/MoreInfo.js | 6 +-
.../bitcoinConnect/CoursePaymentButton.js | 8 +-
.../bitcoinConnect/ResourcePaymentButton.js | 8 +-
src/components/forms/course/LessonSelector.js | 10 +-
src/components/onboarding/WelcomeModal.js | 9 +-
src/components/profile/UserBadges.js | 12 +-
src/components/profile/UserProfileCard.js | 10 +-
.../profile/subscription/CalendlyEmbed.js | 8 +-
.../subscription/CancelSubscription.js | 10 +-
.../subscription/LightningAddressForm.js | 8 +-
.../profile/subscription/Nip05Form.js | 8 +-
.../profile/subscription/RenewSubscription.js | 7 +-
.../profile/subscription/SubscribeModal.js | 9 +-
src/components/ui/Modal.js | 112 ++++++++++++++++++
14 files changed, 173 insertions(+), 52 deletions(-)
create mode 100644 src/components/ui/Modal.js
diff --git a/src/components/MoreInfo.js b/src/components/MoreInfo.js
index aed142a..eacd40b 100644
--- a/src/components/MoreInfo.js
+++ b/src/components/MoreInfo.js
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
-import { Dialog } from 'primereact/dialog';
+import Modal from '@/components/ui/Modal';
import { Tooltip } from 'primereact/tooltip';
import useWindowWidth from '@/hooks/useWindowWidth';
@@ -24,7 +24,7 @@ const MoreInfo = ({
/>
{!isMobile && }
-
+
>
);
};
diff --git a/src/components/bitcoinConnect/CoursePaymentButton.js b/src/components/bitcoinConnect/CoursePaymentButton.js
index 49bdb1f..98cb732 100644
--- a/src/components/bitcoinConnect/CoursePaymentButton.js
+++ b/src/components/bitcoinConnect/CoursePaymentButton.js
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import dynamic from 'next/dynamic';
-import { Dialog } from 'primereact/dialog';
+import Modal from '@/components/ui/Modal';
import { LightningAddress } from '@getalby/lightning-tools';
import { track } from '@vercel/analytics';
import { useToast } from '@/hooks/useToast';
@@ -227,11 +227,11 @@ const CoursePaymentButton = ({ lnAddress, amount, onSuccess, onError, courseId }
/>
)}
-
+
);
};
diff --git a/src/components/bitcoinConnect/ResourcePaymentButton.js b/src/components/bitcoinConnect/ResourcePaymentButton.js
index 75617b3..455319b 100644
--- a/src/components/bitcoinConnect/ResourcePaymentButton.js
+++ b/src/components/bitcoinConnect/ResourcePaymentButton.js
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import dynamic from 'next/dynamic';
-import { Dialog } from 'primereact/dialog';
+import Modal from '@/components/ui/Modal';
import { track } from '@vercel/analytics';
import { LightningAddress } from '@getalby/lightning-tools';
import { useToast } from '@/hooks/useToast';
@@ -122,11 +122,11 @@ const ResourcePaymentButton = ({ lnAddress, amount, onSuccess, onError, resource
/>
)}
-
+
>
);
};
diff --git a/src/components/forms/course/LessonSelector.js b/src/components/forms/course/LessonSelector.js
index 52fd7aa..639d6a3 100644
--- a/src/components/forms/course/LessonSelector.js
+++ b/src/components/forms/course/LessonSelector.js
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Dropdown } from 'primereact/dropdown';
import GenericButton from '@/components/buttons/GenericButton';
-import { Dialog } from 'primereact/dialog';
+import Modal from '@/components/ui/Modal';
import { Accordion, AccordionTab } from 'primereact/accordion';
import EmbeddedDocumentForm from '@/components/forms/course/embedded/EmbeddedDocumentForm';
import EmbeddedVideoForm from '@/components/forms/course/embedded/EmbeddedVideoForm';
@@ -233,23 +233,23 @@ const LessonSelector = ({
-
+
-
+
- Recurring {user?.role?.subscriptionType || 'monthly'} subscription will AUTO renew on {subscribedUntil?.toLocaleDateString()}
+ Recurring {user?.role?.subscriptionType || 'monthly'} subscription will AUTO renew on {subscribedUntil ? subscribedUntil.toLocaleDateString() : 'N/A'}
)}
diff --git a/src/constants/subscriptionPeriods.js b/src/constants/subscriptionPeriods.js
new file mode 100644
index 0000000..76e5f45
--- /dev/null
+++ b/src/constants/subscriptionPeriods.js
@@ -0,0 +1,36 @@
+// Constants for subscription periods to maintain consistency across the application
+export const SUBSCRIPTION_PERIODS = {
+ MONTHLY: {
+ DAYS: 30,
+ BUFFER_HOURS: 1, // Buffer time for expiration checks
+ },
+ YEARLY: {
+ DAYS: 365,
+ BUFFER_HOURS: 1, // Buffer time for expiration checks
+ }
+};
+
+// Helper to calculate expiration date (for UI display)
+export const calculateExpirationDate = (startDate, subscriptionType) => {
+ const periodDays = subscriptionType === 'yearly'
+ ? SUBSCRIPTION_PERIODS.YEARLY.DAYS
+ : SUBSCRIPTION_PERIODS.MONTHLY.DAYS;
+
+ return new Date(startDate.getTime() + periodDays * 24 * 60 * 60 * 1000);
+};
+
+// Helper to check if subscription has expired (for backend logic)
+export const hasSubscriptionExpired = (lastPaymentDate, subscriptionType) => {
+ if (!lastPaymentDate) return true;
+
+ const now = new Date();
+ const period = subscriptionType === 'yearly'
+ ? SUBSCRIPTION_PERIODS.YEARLY
+ : SUBSCRIPTION_PERIODS.MONTHLY;
+
+ const expirationTime = lastPaymentDate.getTime() +
+ (period.DAYS * 24 * 60 * 60 * 1000) +
+ (period.BUFFER_HOURS * 60 * 60 * 1000);
+
+ return now.getTime() > expirationTime;
+};
\ No newline at end of file
diff --git a/src/db/models/userModels.js b/src/db/models/userModels.js
index dd97c4e..1f97e91 100644
--- a/src/db/models/userModels.js
+++ b/src/db/models/userModels.js
@@ -1,4 +1,5 @@
import prisma from '../prisma';
+import { SUBSCRIPTION_PERIODS } from '@/constants/subscriptionPeriods';
export const getAllUsers = async () => {
return await prisma.user.findMany({
@@ -205,12 +206,16 @@ export const findExpiredSubscriptions = async () => {
try {
const now = new Date();
- // Define expiration periods
+ // Use the constants for expiration periods
const monthlyExpiration = new Date(
- now.getTime() - 30 * 24 * 60 * 60 * 1000 - 1 * 60 * 60 * 1000
+ now.getTime() -
+ (SUBSCRIPTION_PERIODS.MONTHLY.DAYS * 24 * 60 * 60 * 1000) -
+ (SUBSCRIPTION_PERIODS.MONTHLY.BUFFER_HOURS * 60 * 60 * 1000)
);
const yearlyExpiration = new Date(
- now.getTime() - 365 * 24 * 60 * 60 * 1000 - 1 * 60 * 60 * 1000
+ now.getTime() -
+ (SUBSCRIPTION_PERIODS.YEARLY.DAYS * 24 * 60 * 60 * 1000) -
+ (SUBSCRIPTION_PERIODS.YEARLY.BUFFER_HOURS * 60 * 60 * 1000)
);
// Find expired subscriptions of both types
From ab7b5fc273689e3abb33782e17d955c388d52ce7 Mon Sep 17 00:00:00 2001
From: austinkelsay
Date: Wed, 14 May 2025 16:55:23 -0500
Subject: [PATCH 5/7] standardize subscription periods and add null date
handling
---
.../profile/subscription/UserSubscription.js | 18 ++++++++++++------
src/pages/about.js | 3 ++-
2 files changed, 14 insertions(+), 7 deletions(-)
diff --git a/src/components/profile/subscription/UserSubscription.js b/src/components/profile/subscription/UserSubscription.js
index 3b29952..b0d734e 100644
--- a/src/components/profile/subscription/UserSubscription.js
+++ b/src/components/profile/subscription/UserSubscription.js
@@ -17,6 +17,7 @@ import Nip05Form from '@/components/profile/subscription/Nip05Form';
import LightningAddressForm from '@/components/profile/subscription/LightningAddressForm';
import RenewSubscription from '@/components/profile/subscription/RenewSubscription';
import { SelectButton } from 'primereact/selectbutton';
+import { SUBSCRIPTION_PERIODS, calculateExpirationDate } from '@/constants/subscriptionPeriods';
const UserSubscription = () => {
const { data: session, update } = useSession();
@@ -52,13 +53,18 @@ const UserSubscription = () => {
useEffect(() => {
if (user && user.role) {
setSubscribed(user.role.subscribed);
- const subscribedAt = new Date(user.role.lastPaymentAt);
- // Calculate subscription end date based on type
- const daysToAdd = user.role.subscriptionType === 'yearly' ? 365 : 31;
- const subscribedUntil = new Date(subscribedAt.getTime() + daysToAdd * 24 * 60 * 60 * 1000);
+ if (user.role.lastPaymentAt) {
+ const subscribedAt = new Date(user.role.lastPaymentAt);
+
+ // Use the common helper to calculate expiration date
+ const subscribedUntil = calculateExpirationDate(subscribedAt, user.role.subscriptionType || 'monthly');
+
+ setSubscribedUntil(subscribedUntil);
+ } else {
+ setSubscribedUntil(null);
+ }
- setSubscribedUntil(subscribedUntil);
if (user.role.subscriptionExpiredAt) {
const expiredAt = new Date(user.role.subscriptionExpiredAt);
setSubscriptionExpiredAt(expiredAt);
@@ -243,7 +249,7 @@ const UserSubscription = () => {
Current Plan: {user?.role?.subscriptionType || 'monthly'} subscription
- Renews on: {subscribedUntil?.toLocaleDateString()}
+ Renews on: {subscribedUntil ? subscribedUntil.toLocaleDateString() : 'N/A'}