|
1 | 1 | // Cloud-only (billing, browser): completing the Team free-trial checkout should |
2 | | -// leave the billing page showing the new plan WITHOUT a manual reload. |
| 2 | +// leave the billing page showing the new plan WITHOUT a manual reload, and |
| 3 | +// without ever flashing the stale upgrade CTA. |
3 | 4 | // |
4 | | -// Repro of a reported bug: a user starts the free trial, completes Stripe |
5 | | -// checkout, and is redirected back to the plans page, which STILL shows the |
6 | | -// upgrade/"Start free trial" call to action. A manual reload then shows the |
7 | | -// active trial. The cause is client-side: autumn-js fetches the customer once |
8 | | -// on load (staleTime 60s, refetchOnWindowFocus off) and the redirect back from |
9 | | -// Stripe lands before Autumn has processed Stripe's webhook, so that single |
10 | | -// fetch sees the old plan and the page never refetches on its own. |
| 5 | +// Guards a reported bug + its fix: a user starts the free trial, completes |
| 6 | +// Stripe checkout, and is redirected back to the plans page. The redirect lands |
| 7 | +// before Autumn has processed Stripe's webhook, and autumn-js fetches the |
| 8 | +// customer once on load (staleTime 60s, refetchOnWindowFocus off), so the page |
| 9 | +// used to show the old plan and the "Start free trial" CTA until a manual |
| 10 | +// reload. The fix tags the checkout return URL with the purchased plan, then on |
| 11 | +// return shows that plan as "Activating" while it refetches until the webhook |
| 12 | +// lands, resolving to "Your plan" with no reload. |
11 | 13 | // |
12 | | -// The emulator models this faithfully: completing the hosted checkout redirects |
13 | | -// back immediately but does NOT activate the subscription; the activation lands |
14 | | -// only when the webhook settles (autumn.settleCheckout). The reload control |
15 | | -// proves the backend is consistent, isolating the failure to the stale client. |
| 14 | +// The emulator models the race faithfully: completing the hosted checkout |
| 15 | +// redirects back immediately but does NOT activate the subscription; activation |
| 16 | +// lands only when the webhook settles (autumn.settleCheckout), which this test |
| 17 | +// triggers to control the exact moment the backend becomes consistent. |
16 | 18 | import { expect } from "@effect/vitest"; |
17 | 19 | import { Effect } from "effect"; |
18 | 20 |
|
@@ -63,36 +65,25 @@ scenario( |
63 | 65 |
|
64 | 66 | await step("Complete checkout and return to the plans page", async () => { |
65 | 67 | await page.locator("button.checkout-pay-btn").click(); |
66 | | - // Checkout completes and redirects back to the success_url (the plans |
67 | | - // page). The webhook has NOT landed yet, so the trial is still offered: |
68 | | - // exactly the window the bug lives in. |
69 | 68 | await page.waitForURL(/billing\/plans/, { timeout: 30_000 }); |
70 | | - await startTrial.waitFor(); |
| 69 | + // The webhook has NOT landed yet, but the page knows from the return |
| 70 | + // marker which plan was purchased, so it shows that plan as activating |
| 71 | + // rather than the stale upgrade CTA (which would read as if nothing |
| 72 | + // happened). This is the key user-facing guarantee. |
| 73 | + await teamCard.getByText("Activating", { exact: true }).waitFor({ timeout: 10_000 }); |
| 74 | + expect(await startTrial.count(), "the stale trial CTA is not shown on return").toBe(0); |
71 | 75 | }); |
72 | 76 |
|
73 | 77 | // The Stripe webhook reaches Autumn: the customer is now on the Team trial. |
74 | | - // From here on the billing backend is authoritative-consistent. |
75 | 78 | await Effect.runPromise(autumn.settleCheckout(sessionId)); |
76 | 79 |
|
77 | | - // The page the user was returned to never reloads. If the UI refetched |
78 | | - // after returning from checkout it would drop the trial CTA within a few |
79 | | - // seconds; today it does not, so this stays visible. |
80 | | - const reflectedWithoutReload = await startTrial |
81 | | - .waitFor({ state: "hidden", timeout: 8_000 }) |
82 | | - .then(() => true) |
83 | | - .catch(() => false); |
84 | | - |
85 | | - // Control: a manual reload surfaces the active trial, proving the backend |
86 | | - // was consistent all along and the only thing missing was a client refetch. |
87 | | - await step("A manual reload shows the active trial", async () => { |
88 | | - await page.reload({ waitUntil: "networkidle" }); |
| 80 | + // Without any reload, the activating state resolves to the active plan as |
| 81 | + // the polled refetch picks up the now-consistent backend. |
| 82 | + await step("The plan resolves to active without a reload", async () => { |
89 | 83 | await teamCard.getByText("Current plan").waitFor({ timeout: 15_000 }); |
| 84 | + await teamCard.getByText("Your plan").waitFor({ timeout: 5_000 }); |
90 | 85 | }); |
91 | | - |
92 | | - expect( |
93 | | - reflectedWithoutReload, |
94 | | - "the plans page reflects the completed checkout without a manual reload", |
95 | | - ).toBe(true); |
| 86 | + expect(await startTrial.count(), "the upgrade CTA never returns").toBe(0); |
96 | 87 | }); |
97 | 88 | }), |
98 | 89 | ); |
0 commit comments