mirror of
https://github.com/vernu/textbee.git
synced 2026-07-30 17:07:46 -04:00
/checkout/pro and /checkout/scale failed with "Plan cannot be purchased"
for every user since the Next 16 upgrade (ea1b00c).
The checkout page still typed `params` as a plain object and read
`params.planName` synchronously. Next 16 removed the sync compat shim, so
the value was undefined, axios dropped the key, and the API ran
findOne({ name: undefined }). Mongoose 9 returns null for that where
older versions stripped the key, so the guard threw a message about a
misconfigured plan for a request that never named one. Plan data and
Polar product ids were correct throughout.
- unwrap params with React's use() and drop the `as string` cast that
suppressed the type error
- split the conflated guard into PLAN_NAME_REQUIRED, PLAN_NOT_FOUND and
the existing unpurchasable case, each logged server side
- stop retrying a rejected checkout twice: a 400 is not transient
- mark the Polar product id indexes sparse, they have been failing to
build with E11000 on every boot because most plans leave them unset
Checkout now defaults to yearly. A URL that names an interval still
redirects straight through, so the marketing funnel is unchanged; the
in-app CTAs that name none get an interval chooser with yearly
preselected instead of a silent monthly default.
Also redesigns the checkout states, which were the only place in the
dashboard hardcoding gray-100/white and so rendering a light slab in dark
mode, and the plan picker, which painted the accent on every tier.
Tests: a checkout e2e spec asserting the request body carries planName,
which is what would have caught this, plus API cases pinning the three
guard failures apart.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
61 lines
2.3 KiB
TypeScript
61 lines
2.3 KiB
TypeScript
import type { Page, Route } from '@playwright/test'
|
|
import {
|
|
mockApiKeys,
|
|
mockBillingPlans,
|
|
mockDevices,
|
|
mockMessages,
|
|
mockStats,
|
|
mockSubscription,
|
|
mockUser,
|
|
mockWebhookNotifications,
|
|
mockWebhooks,
|
|
} from '../test/fixtures'
|
|
|
|
const json = (route: Route, body: unknown, status = 200) =>
|
|
route.fulfill({
|
|
status,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(body),
|
|
})
|
|
|
|
// Intercept EVERY call to the backend (`/api/v1/**`) and serve fixtures. Because
|
|
// the app is pointed at a backend host that nothing listens on, this guarantees
|
|
// no request ever reaches a real backend during e2e.
|
|
export type MockApiOverrides = {
|
|
/** Serve a different subscription payload, e.g. the free-user shape. */
|
|
subscription?: unknown
|
|
/** Fail POST /billing/checkout with this message, to exercise the error state. */
|
|
checkoutError?: string
|
|
}
|
|
|
|
export async function mockApi(page: Page, overrides: MockApiOverrides = {}) {
|
|
await page.route('**/api/v1/**', (route) => {
|
|
const path = new URL(route.request().url()).pathname.replace('/api/v1', '')
|
|
|
|
// Kept in-origin so following the redirect does not leave the test app.
|
|
if (path === '/billing/checkout') {
|
|
if (overrides.checkoutError) {
|
|
return json(route, { message: overrides.checkoutError }, 400)
|
|
}
|
|
return json(route, { redirectUrl: '/dashboard?polar-checkout-mock=1' })
|
|
}
|
|
|
|
if (path === '/auth/who-am-i') return json(route, { data: mockUser })
|
|
if (path === '/billing/current-subscription')
|
|
return json(route, overrides.subscription ?? mockSubscription)
|
|
if (path === '/billing/plans') return json(route, { data: mockBillingPlans })
|
|
if (path === '/gateway/devices') return json(route, { data: mockDevices })
|
|
if (path === '/gateway/stats') return json(route, { data: mockStats })
|
|
if (path === '/webhooks') return json(route, { data: mockWebhooks })
|
|
if (path === '/webhooks/notifications')
|
|
return json(route, mockWebhookNotifications)
|
|
if (path === '/auth/api-keys') return json(route, { data: mockApiKeys })
|
|
if (/\/gateway\/devices\/[^/]+\/(messages|get-received-sms)/.test(path))
|
|
return json(route, mockMessages)
|
|
|
|
// Any unmapped backend call still gets a benign mocked response so the test
|
|
// cannot fall through to a real backend.
|
|
return json(route, { data: [] })
|
|
})
|
|
}
|