From 2bb07b128aa7a11f9802116c8274ed3d8a02aad5 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:02:46 +0000 Subject: [PATCH] Fix Proceed to Payment dead ends on the subscription lock screen 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 --- .../components/subscription-submit-button.tsx | 36 ++++++++++++------- src/lib/relay/environment.ts | 17 +++++++++ 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/app/(app)/settings/billing-usage/subscription/components/subscription-submit-button.tsx b/src/app/(app)/settings/billing-usage/subscription/components/subscription-submit-button.tsx index e86c0f64..fb9091d5 100644 --- a/src/app/(app)/settings/billing-usage/subscription/components/subscription-submit-button.tsx +++ b/src/app/(app)/settings/billing-usage/subscription/components/subscription-submit-button.tsx @@ -12,7 +12,7 @@ interface SubscriptionSubmitButtonProps { packageUpdates: PackageUpdateInput[]; /** Desired end-state for the checkout flow. */ checkoutProducts: ProductCheckoutInput[]; - /** True when a Custom Amount has an empty/invalid quantity (update flow only). */ + /** True when a Custom Amount has an empty/invalid quantity. */ hasInvalidCustom: boolean; /** Extra classes for the button (e.g. `w-full` for the mobile action bar). */ className?: string; @@ -20,8 +20,8 @@ interface SubscriptionSubmitButtonProps { /** * Renders the correct submit action for the current subscription state: - * - no active paid subscription → "Create Subscription" (Stripe Checkout); no - * diff/validation gating — there is nothing to compare against. + * - no active paid subscription → "Proceed to Payment" (Stripe Checkout); + * disabled when there is nothing to buy, validated on click. * - active paid subscription → "Update Subscription"; disabled when the * selection equals the current plan, validated on click. */ @@ -38,17 +38,31 @@ export function SubscriptionSubmitButton({ const isPending = updateSubscription.isPending || createCheckout.isPending; + const invalidCustomToast = () => { + toast({ + title: 'Invalid amount', + description: 'Enter a valid number for the custom package.', + variant: 'destructive', + }); + }; + if (needsCheckout) { + const handleCheckout = () => { + if (hasInvalidCustom) { + invalidCustomToast(); + return; + } + if (!checkoutProducts.length) return; + createCheckout.mutate({ products: checkoutProducts }); + }; + return ( @@ -57,11 +71,7 @@ export function SubscriptionSubmitButton({ const handleUpdate = () => { if (hasInvalidCustom) { - toast({ - title: 'Invalid amount', - description: 'Enter a valid number for the custom package.', - variant: 'destructive', - }); + invalidCustomToast(); return; } if (!packageUpdates.length) return; diff --git a/src/lib/relay/environment.ts b/src/lib/relay/environment.ts index 2377ad1a..84e5adf0 100644 --- a/src/lib/relay/environment.ts +++ b/src/lib/relay/environment.ts @@ -380,6 +380,23 @@ const fetchRelay: FetchFunction = async (request, variables) => { if (json.errors) { console.error('[Relay] GraphQL errors:', json.errors); detectTrialExpiredFromGraphqlErrors(json.errors); + + // The server returned errors and no data. Relay would otherwise reject with + // its internal "No data returned for operation ..." message, which reaches + // the user verbatim through a mutation's `onError` toast (e.g. checkout on a + // mutation like `createCheckoutSession`, whose `CheckoutResult` payload has + // no `errors` field). Throw the readable server message so `onError` shows + // that instead. Partial responses (data present with field errors) fall + // through unchanged. + if (json.data == null) { + const message = json.errors + .map((error: { message?: string }) => error?.message) + .filter(Boolean) + .join('. '); + if (message) { + throw new Error(message); + } + } } return json;