implement free plan age verification via Stripe SetupIntent; personalize soul demo greeting with user name and timezone
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m49s

This commit is contained in:
2026-05-11 02:03:39 -05:00
parent ac2d00d653
commit c966f2b455
7 changed files with 167 additions and 74 deletions
+36 -23
View File
@@ -243,12 +243,7 @@ test('[free] auth-section visible on load (account creation flow)', async ({ pag
await expect(page.locator('#auth-section')).toBeVisible();
});
test('[free] free-success panel hidden on load', async ({ page }) => {
await page.goto('/checkout?plan=free');
await expect(page.locator('#free-success')).toBeHidden();
});
test('[free] no payment-section or it is hidden', async ({ page }) => {
test('[free] payment-section hidden on load (shown after auth)', async ({ page }) => {
await page.goto('/checkout?plan=free');
const ps = page.locator('#payment-section');
if (await ps.count() > 0) {
@@ -256,6 +251,11 @@ test('[free] no payment-section or it is hidden', async ({ page }) => {
}
});
test('[free] payment-element container present (Stripe mounts here)', async ({ page }) => {
await page.goto('/checkout?plan=free');
await expect(page.locator('#payment-element')).toBeAttached();
});
test('[professional] payment-section visible on load', async ({ page }) => {
await page.goto('/checkout?plan=professional');
await expect(page.locator('#payment-section')).toBeVisible();
@@ -301,6 +301,7 @@ for (const plan of ['professional', 'founding'] as const) {
test('[free] submit with empty email shows auth error', async ({ page }) => {
await mockSupabaseConfig(page);
await page.goto('/checkout?plan=free');
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
await page.locator('.checkout-email-btn').click();
const msg = page.locator('#auth-message');
await expect(msg).toBeVisible({ timeout: 4000 });
@@ -311,6 +312,7 @@ test('[free] submit with empty email shows auth error', async ({ page }) => {
test('[free] submit with password < 8 chars shows length error', async ({ page }) => {
await mockSupabaseConfig(page);
await page.goto('/checkout?plan=free');
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
await page.fill('#auth-email', 'test@example.com');
await page.fill('#auth-password', 'short');
await page.locator('.checkout-email-btn').click();
@@ -323,6 +325,7 @@ test('[free] submit with password < 8 chars shows length error', async ({ page }
test('[free] submit with email only (no password) shows error', async ({ page }) => {
await mockSupabaseConfig(page);
await page.goto('/checkout?plan=free');
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
await page.fill('#auth-email', 'test@example.com');
// leave password empty
await page.locator('.checkout-email-btn').click();
@@ -339,6 +342,7 @@ test('[free] initial button says "Create account"', async ({ page }) => {
test('[free] clicking "Sign in" link changes button text to "Sign in"', async ({ page }) => {
await page.goto('/checkout?plan=free');
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
await page.click('a[onclick*="showSignIn"]');
await expect(page.locator('.checkout-email-btn')).toContainText('Sign in');
});
@@ -350,16 +354,17 @@ test('[free] divider label changes for email mode', async ({ page }) => {
// ─── Mocked free-plan auth flows ──────────────────────────────────────────────
test('[free] successful sign-up → free-success shown, auth-section hidden', async ({ page }) => {
test('[free] successful sign-up → payment-section shown, auth-section hidden', async ({ page }) => {
await mockSupabaseConfig(page);
await mockSignUpSuccess(page);
await page.goto('/checkout?plan=free');
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
await page.fill('#auth-email', 'newuser@example.com');
await page.fill('#auth-password', 'password123');
await page.locator('.checkout-email-btn').click();
await expect(page.locator('#free-success')).toBeVisible({ timeout: 6000 });
await expect(page.locator('#payment-section')).toBeVisible({ timeout: 6000 });
await expect(page.locator('#auth-section')).toBeHidden();
});
@@ -367,6 +372,7 @@ test('[free] sign-up email-confirm-required → shows check-email message', asyn
await mockSupabaseConfig(page);
await mockSignUpEmailConfirmRequired(page);
await page.goto('/checkout?plan=free');
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
await page.fill('#auth-email', 'confirm@example.com');
await page.fill('#auth-password', 'password123');
@@ -378,23 +384,25 @@ test('[free] sign-up email-confirm-required → shows check-email message', asyn
expect(text.toLowerCase()).toMatch(/email|confirm|check/);
});
test('[free] sign-in success (via toggle) → free-success shown', async ({ page }) => {
test('[free] sign-in success (via toggle) → payment-section shown', async ({ page }) => {
await mockSupabaseConfig(page);
await mockSignInSuccess(page);
await page.goto('/checkout?plan=free');
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
await page.click('a[onclick*="showSignIn"]');
await page.fill('#auth-email', 'existing@example.com');
await page.fill('#auth-password', 'password123');
await page.locator('.checkout-email-btn').click();
await expect(page.locator('#free-success')).toBeVisible({ timeout: 6000 });
await expect(page.locator('#payment-section')).toBeVisible({ timeout: 6000 });
});
test('[free] sign-in error → shows error message, form stays visible', async ({ page }) => {
await mockSupabaseConfig(page);
await mockSignInError(page, 'Invalid login credentials');
await page.goto('/checkout?plan=free');
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
await page.click('a[onclick*="showSignIn"]');
await page.fill('#auth-email', 'wrong@example.com');
@@ -465,21 +473,26 @@ for (const plan of ['professional', 'founding'] as const) {
});
}
// ─── /api/checkout endpoint ───────────────────────────────────────────────────
// ─── /api/payment-intent endpoint ────────────────────────────────────────────
test('POST /api/checkout free plan returns no_payment_required', async ({ request }) => {
const res = await request.post('/api/checkout', {
test('POST /api/payment-intent free plan returns setup_mode (age verification)', async ({ request }) => {
const res = await request.post('/api/payment-intent', {
data: JSON.stringify({ plan: 'free', email: 'test@example.com' }),
headers: { 'Content-Type': 'application/json' },
});
// Free plan never calls Stripe — must be fast and return the flag
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.no_payment_required ?? body.free).toBeTruthy();
// Free plan creates a SetupIntent for age verification — must not 500
expect(res.status()).toBeLessThan(500);
if (res.status() === 200) {
const body = await res.json();
// Either setup_mode (success) or an error from Stripe (unconfigured env) — both valid
expect('setup_mode' in body || 'client_secret' in body || 'error' in body).toBeTruthy();
// Must NOT return the old no_payment_required flag
expect(body.no_payment_required).toBeFalsy();
}
});
test('POST /api/checkout professional returns client_secret or config error (not 500)', async ({ request }) => {
const res = await request.post('/api/checkout', {
test('POST /api/payment-intent professional returns client_secret or config error (not 500)', async ({ request }) => {
const res = await request.post('/api/payment-intent', {
data: JSON.stringify({ plan: 'professional', email: 'test@example.com', name: 'Test User' }),
headers: { 'Content-Type': 'application/json' },
});
@@ -490,16 +503,16 @@ test('POST /api/checkout professional returns client_secret or config error (not
}
});
test('POST /api/checkout founding returns client_secret or config error (not 500)', async ({ request }) => {
const res = await request.post('/api/checkout', {
test('POST /api/payment-intent founding returns client_secret or config error (not 500)', async ({ request }) => {
const res = await request.post('/api/payment-intent', {
data: JSON.stringify({ plan: 'founding', email: 'test@example.com', name: 'Test User' }),
headers: { 'Content-Type': 'application/json' },
});
expect(res.status()).toBeLessThan(500);
});
test('POST /api/checkout empty body returns 4xx not 500', async ({ request }) => {
const res = await request.post('/api/checkout', { data: {} });
test('POST /api/payment-intent empty body returns 4xx or config error (not 500)', async ({ request }) => {
const res = await request.post('/api/payment-intent', { data: {} });
expect(res.status()).toBeLessThan(500);
});
+12 -6
View File
@@ -144,8 +144,8 @@ test('[founding] Stripe.js script tag present in page', async ({ page }) => {
await expect(stripeScript).toBeAttached();
});
test('[free] Stripe.js is still loaded (though not used)', async ({ page }) => {
// Free plan still includes Stripe.js for forward compat
test('[free] Stripe.js is loaded (used for age verification SetupIntent)', async ({ page }) => {
// Free plan now creates a SetupIntent for age verification
await page.goto('/checkout?plan=free');
const stripeScript = page.locator('script[src*="stripe.com"]');
await expect(stripeScript).toBeAttached();
@@ -186,6 +186,7 @@ test('[professional] payment-message div starts hidden', async ({ page }) => {
test('[professional] buyer-name input is present and fillable', async ({ page }) => {
await page.goto('/checkout?plan=professional');
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
await expect(page.locator('#buyer-name')).toBeAttached();
await page.fill('#buyer-name', 'Test User');
expect(await page.locator('#buyer-name').inputValue()).toBe('Test User');
@@ -193,6 +194,7 @@ test('[professional] buyer-name input is present and fillable', async ({ page })
test('[professional] buyer-email input is present and fillable', async ({ page }) => {
await page.goto('/checkout?plan=professional');
await page.waitForFunction(() => typeof (window as any).signUpWithEmail === 'function', { timeout: 10000 });
await expect(page.locator('#buyer-email')).toBeAttached();
await page.fill('#buyer-email', 'test@example.com');
expect(await page.locator('#buyer-email').inputValue()).toBe('test@example.com');
@@ -416,14 +418,18 @@ test('[professional] successful payment: submit-btn shows spinner then loading s
// ─── /api/payment-intent endpoint contracts ───────────────────────────────────
test('POST /api/payment-intent free plan returns no_payment_required', async ({ request }) => {
test('POST /api/payment-intent free plan returns setup_mode (age verification)', async ({ request }) => {
const res = await request.post('/api/payment-intent', {
data: JSON.stringify({ plan: 'free', email: 'test@example.com' }),
headers: { 'Content-Type': 'application/json' },
});
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.no_payment_required ?? body.free).toBeTruthy();
// Free plan creates a SetupIntent for age verification — must not 500
expect(res.status()).toBeLessThan(500);
if (res.status() === 200) {
const body = await res.json();
expect('setup_mode' in body || 'client_secret' in body || 'error' in body).toBeTruthy();
expect(body.no_payment_required).toBeFalsy();
}
});
test('POST /api/payment-intent professional returns client_secret or stripe error (not 500)', async ({ request }) => {