Fix Proceed to Payment dead ends on the subscription lock screen - #274
posthog[bot] wants to merge 2 commits into
Conversation
The checkout branch of SubscriptionSubmitButton returned silently when there were no checkout products, so the button looked live but did nothing. It also ignored hasInvalidCustom, unlike the update branch. - Disable the button when there is nothing to buy. - Toast on an invalid custom amount before starting checkout. - Surface the GraphQL error message from the Relay network layer when the server returns errors and no data, instead of Relay's internal "No data returned for operation ..." wording. Generated-By: PostHog Desktop Task-Id: 7a768f5c-ed00-4e07-b773-0b2d42a1c090
…fixbilling-make-proceed-to-payment-fail-247b87 # Conflicts: # src/app/(app)/settings/billing-usage/subscription/components/subscription-submit-button.tsx # src/lib/relay/environment.ts
pavlo-flamingo
left a comment
There was a problem hiding this comment.
Merged main into the branch (ba6f1b8). The conflicts were with #137 (spend cap + deferred Stripe tab in the submit button) and #311 (subscription gate in the Relay fetch); the resolution keeps main's bodies, so this PR's delta is now the disabled guard, the hoisted handleCheckout, and the Relay-layer throw. type-check, lint:ci and format are green on the merge.
1. src/lib/relay/environment.ts: the throw regresses error toasts app-wide. Please drop it.
Problem. throw new Error(messages.join('. ')) replaces the error Relay would have thrown. Relay's error carries error.source.errors (the structured GraphQL errors), and getRelayErrorMessage in src/lib/handle-api-error.ts reads exactly that: it drops graphql-java's "The field at path … was declared as a non null type …" wrapper, dedups, and joins. 16 call sites (scripts, schedules, onboarding) rely on it.
Effect. With source gone those toasts fall back to the raw joined string. Every billing mutation returns a non-null type (CheckoutResult!, UpdateSubscriptionResult!, SubscriptionDetail!), so graphql-java emits the non-null wrapper next to the real error, and the checkout toast this PR sets out to fix would read ". The field at path '/createCheckoutSession' was declared as a non null type, but …". The note "this only replaces the message text" is not accurate: it also strips source (errors, operation, variables) and the RelayNetwork error name.
Fix. Leave the network layer alone and use the existing helper in the hook: getRelayErrorMessage(err, 'Failed to start checkout') in use-create-checkout-session.ts. The same raw err.message pattern sits in use-update-subscription.ts, use-update-ai-spend-cap.ts, use-cancel-subscription.ts and use-resume-subscription.ts. billing-usage/hooks/extract-graphql-error-message.ts is a weaker local copy of the same helper and can go with it.
2. subscription-submit-button.tsx: the disabled guard is right, the rationale is stale.
On main buildCheckoutProduct returns a product for pay-as-you-go as well, so checkoutProducts is empty only before the plan picker's first report (catalog still loading, or no device product in the catalog). A disabled button beats a live-looking no-op there, so keep the guard; just fix the description, "pay-as-you-go only" is no longer an empty selection. The if (!checkoutProducts.length) return; inside the handler is now unreachable; harmless.
3. Test plan
npm run lint:biome no longer exists (Biome was replaced on 2026-09-02). The gates are npm run lint:ci, npm run format and npm run type-check.
|
Superseded by #406. The |
Problem
if (!checkoutProducts.length) return;but never disabled the button, so it looked live and gave no spinner, toast, or redirect. It also ignoredhasInvalidCustom, which the update branch already honors.CheckoutResulthas noerrorsfield, so a rejected checkout came back as errors with no data. Relay then rejected with its internalNo data returned for operation ...string, which landed verbatim in the toast. The readable server message was onlyconsole.error-logged.Changes
checkoutProducts.length === 0), matching the update branchonErrorshows it; partial responses (data present) are unchangedNotes
onError/error boundaries — this only replaces the message text.useProductSelectionfires on mount and populatescheckoutProductsfor every non-PAYG selection, so the button enables normally and only disables when the selection is genuinely empty (e.g. pay-as-you-go only).Test plan
npm run type-checkcleannpm run lint:biomeclean on changed filesCreated with PostHog Desktop from this inbox report.