mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
feat(billing): defer the cardless trial for invited collaborators
An account that signs up through an invitation works on the inviter's billing, so handing it a trial at signup spent its only trial before it owned anything. The trial is now held back for collaborators and claimed only explicitly: a Start Free Trial button on the new-workspace and billing screens calls the new POST /api/billing/trial endpoint, which grants the once-per-account trial atomically. Nothing starts the clock as a side effect, and pure collaborators no longer see a trial-ending banner about work that is not theirs.
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
// fails a test rather than a security review.
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import { InvitationScope } from '@prisma/client';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import nodemailer from 'nodemailer';
|
||||
import { db } from '@/lib/db';
|
||||
@@ -20,7 +21,7 @@ import { GET as verifyEmail } from '@/app/api/auth/verify-email/route';
|
||||
import { POST as resendVerification } from '@/app/api/auth/verify-email/resend/route';
|
||||
import { apiRequest, callRoute, readData, readError } from '../helpers/request';
|
||||
import { mailTo, sentMail } from '../helpers/mail';
|
||||
import { createUser } from '../factories';
|
||||
import { addWorkspaceMember, createInvitation, createUser, seedProject } from '../factories';
|
||||
|
||||
const TWO_HOURS_MS = 2 * 60 * 60 * 1000;
|
||||
const MINUTE_MS = 60 * 1000;
|
||||
@@ -117,6 +118,54 @@ describe('consumeVerificationToken', () => {
|
||||
expect(days).toBe(7);
|
||||
});
|
||||
|
||||
// An invited collaborator works inside the inviter's workspace on the inviter's
|
||||
// billing, so a trial handed over here would be spent before they had seen the
|
||||
// product on an account of their own, and `billingTrialConsumedAt` is never
|
||||
// cleared. It waits until they create a workspace of their own.
|
||||
it('holds the trial back for somebody who verified as an invited member', async () => {
|
||||
const host = await seedProject();
|
||||
const user = await createUser({
|
||||
email: '[email protected]',
|
||||
emailVerified: null,
|
||||
trialEndsAt: null,
|
||||
billingTrialConsumedAt: null,
|
||||
});
|
||||
await addWorkspaceMember({ workspaceId: host.workspace.id, userId: user.id });
|
||||
const token = await createVerificationToken('[email protected]');
|
||||
|
||||
await consumeVerificationToken(token);
|
||||
|
||||
const verified = await db.user.findUniqueOrThrow({ where: { id: user.id } });
|
||||
expect(verified.emailVerified).toBeInstanceOf(Date);
|
||||
expect(verified.trialEndsAt).toBeNull();
|
||||
expect(verified.billingTrialConsumedAt).toBeNull();
|
||||
});
|
||||
|
||||
// The OAuth half of the same case: the account exists before the invitation is
|
||||
// accepted, so the still-open invitation is the only signal there is.
|
||||
it('holds the trial back while an invitation to that address is still open', async () => {
|
||||
const host = await seedProject();
|
||||
const user = await createUser({
|
||||
email: '[email protected]',
|
||||
emailVerified: null,
|
||||
trialEndsAt: null,
|
||||
billingTrialConsumedAt: null,
|
||||
});
|
||||
await createInvitation({
|
||||
email: '[email protected]',
|
||||
scope: InvitationScope.WORKSPACE,
|
||||
workspaceId: host.workspace.id,
|
||||
invitedById: host.owner.id,
|
||||
});
|
||||
const token = await createVerificationToken('[email protected]');
|
||||
|
||||
await consumeVerificationToken(token);
|
||||
|
||||
const verified = await db.user.findUniqueOrThrow({ where: { id: user.id } });
|
||||
expect(verified.trialEndsAt).toBeNull();
|
||||
expect(verified.billingTrialConsumedAt).toBeNull();
|
||||
});
|
||||
|
||||
it('does not hand a second trial to an account that already had one', async () => {
|
||||
const consumedAt = new Date('2026-01-01T00:00:00.000Z');
|
||||
const trialEndsAt = new Date('2026-01-08T00:00:00.000Z');
|
||||
|
||||
Reference in New Issue
Block a user