Skip to content

Fix Proceed to Payment dead ends on the subscription lock screen - #274

Closed
posthog[bot] wants to merge 2 commits into
mainfrom
posthog-self-driving/fixbilling-make-proceed-to-payment-fail-247b87
Closed

posthog[bot] wants to merge 2 commits into
mainfrom
posthog-self-driving/fixbilling-make-proceed-to-payment-fail-247b87

Conversation

@posthog

@posthog posthog Bot commented Aug 22, 2026

Copy link
Copy Markdown

Problem

  • A tenant whose trial ended or was canceled has only one forward path — the Proceed to Payment button on the subscription lock screen — and both of its failure modes end that path with no workaround.
  • Silent no-op: the checkout branch ran if (!checkoutProducts.length) return; but never disabled the button, so it looked live and gave no spinner, toast, or redirect. It also ignored hasInvalidCustom, which the update branch already honors.
  • Raw Relay error: CheckoutResult has no errors field, so a rejected checkout came back as errors with no data. Relay then rejected with its internal No data returned for operation ... string, which landed verbatim in the toast. The readable server message was only console.error-logged.

Changes

Symptom Fix
Button looks live but does nothing Disable it when there is nothing to buy (checkoutProducts.length === 0), matching the update branch
Invalid custom amount ignored in checkout Toast and stop before starting checkout, same as the update branch
Relay's internal wording shown to users In the Relay network layer, when the server returns errors and no data, throw the joined GraphQL error message so onError shows it; partial responses (data present) are unchanged

Notes

  • The Relay-layer change is global but safe: an errors-and-null-data response already rejected through onError/error boundaries — this only replaces the message text.
  • useProductSelection fires on mount and populates checkoutProducts for 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-check clean
  • npm run lint:biome clean on changed files

Created with PostHog Desktop from this inbox report.

posthog Bot and others added 2 commits August 22, 2026 17:02
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 pavlo-flamingo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@pavlo-flamingo

Copy link
Copy Markdown
Contributor

Superseded by #406. The disabled guard on Proceed to Payment and the readable checkout error land there as reviewed: getRelayErrorMessage in the billing hooks, no Relay-layer throw (it strips error.source, which the helper and 16 other call sites read).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant