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
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m49s
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user