feat: dashboard Second Brain, essai 7 jours et vérification e-mail
Rendre le dashboard actionnable (inbox, peek, carte mentale), aligner la facturation sur l’essai 7 jours, et bloquer le login e-mail tant que l’adresse n’est pas confirmée. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2
memento-note/lib/billing/trial-constants.ts
Normal file
2
memento-note/lib/billing/trial-constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
/** Free trial length on first Pro / Business checkout. Safe for client imports. */
|
||||
export const SUBSCRIPTION_TRIAL_DAYS = 7
|
||||
55
memento-note/lib/billing/trial-reminder-email.ts
Normal file
55
memento-note/lib/billing/trial-reminder-email.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { sendEmail } from '@/lib/mail'
|
||||
|
||||
function formatTrialEnd(date: Date, locale: string): string {
|
||||
try {
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
}).format(date)
|
||||
} catch {
|
||||
return date.toISOString().slice(0, 10)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort reminder ~3 days before Stripe ends a trial.
|
||||
* Failures are logged by the caller; never throw to the webhook.
|
||||
*/
|
||||
export async function sendTrialEndingReminder(opts: {
|
||||
to: string
|
||||
name?: string | null
|
||||
trialEndsAt: Date
|
||||
billingUrl: string
|
||||
locale?: string
|
||||
}): Promise<{ success: boolean; error?: string }> {
|
||||
const locale = opts.locale?.startsWith('fr') ? 'fr' : 'en'
|
||||
const endLabel = formatTrialEnd(opts.trialEndsAt, locale === 'fr' ? 'fr-FR' : 'en-US')
|
||||
const greet = opts.name?.trim() ? opts.name.trim() : locale === 'fr' ? 'Bonjour' : 'Hi'
|
||||
|
||||
const subject =
|
||||
locale === 'fr'
|
||||
? 'Votre essai Memento se termine bientôt'
|
||||
: 'Your Memento trial is ending soon'
|
||||
|
||||
const html =
|
||||
locale === 'fr'
|
||||
? `
|
||||
<p>${greet},</p>
|
||||
<p>Votre période d'essai Memento se termine le <strong>${endLabel}</strong>.</p>
|
||||
<p>Après cette date, votre abonnement démarrera automatiquement avec le moyen de paiement enregistré.</p>
|
||||
<p>Pour gérer votre abonnement ou votre carte :</p>
|
||||
<p><a href="${opts.billingUrl}">Ouvrir la facturation</a></p>
|
||||
<p>— L'équipe Memento</p>
|
||||
`
|
||||
: `
|
||||
<p>${greet},</p>
|
||||
<p>Your Memento trial ends on <strong>${endLabel}</strong>.</p>
|
||||
<p>After that date, your subscription will start automatically using the payment method on file.</p>
|
||||
<p>To manage your subscription or card:</p>
|
||||
<p><a href="${opts.billingUrl}">Open billing settings</a></p>
|
||||
<p>— The Memento team</p>
|
||||
`
|
||||
|
||||
return sendEmail({ to: opts.to, subject, html })
|
||||
}
|
||||
34
memento-note/lib/billing/trial.ts
Normal file
34
memento-note/lib/billing/trial.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export { SUBSCRIPTION_TRIAL_DAYS } from '@/lib/billing/trial-constants'
|
||||
|
||||
/**
|
||||
* Offer a trial only to users who never held a real Stripe subscription.
|
||||
* Users with cus_mock / price_mock leftovers from local tests are still eligible
|
||||
* if they never had a non-mock stripeSubscriptionId.
|
||||
*/
|
||||
export async function shouldOfferSubscriptionTrial(userId: string): Promise<boolean> {
|
||||
const sub = await prisma.subscription.findUnique({
|
||||
where: { userId },
|
||||
select: {
|
||||
stripeSubscriptionId: true,
|
||||
tier: true,
|
||||
status: true,
|
||||
trialEndsAt: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!sub) return true
|
||||
|
||||
const stripeSubId = sub.stripeSubscriptionId
|
||||
if (stripeSubId && !stripeSubId.startsWith('sub_mock')) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Already on a paid / trial tier locally without a Stripe id (admin override)
|
||||
if (sub.tier !== 'BASIC' && (sub.status === 'ACTIVE' || sub.status === 'TRIALING')) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user