From 5d8cc7b22f2abcfa8a2d363b32076844776dadb4 Mon Sep 17 00:00:00 2001 From: Joaquim d'Souza Date: Thu, 7 May 2026 12:45:02 +0200 Subject: [PATCH] feat: user friendly stripe error message shown to user --- .../src/pages/payment-details.page.tsx | 44 ++++++++++++++++--- .../src/services/rest-resource.service.ts | 6 ++- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/packages/join-flow/src/pages/payment-details.page.tsx b/packages/join-flow/src/pages/payment-details.page.tsx index 3e5f61d5..b358e1ec 100644 --- a/packages/join-flow/src/pages/payment-details.page.tsx +++ b/packages/join-flow/src/pages/payment-details.page.tsx @@ -24,9 +24,38 @@ import { PaymentMethodDDSchema, FormSchema, validate } from "../schema"; import { get as getEnv, getStr as getEnvStr, getPaymentProviders, resolveStripePaymentMethodTypes, PaymentMethod, PaymentProvider } from "../env"; import { loadStripe } from "@stripe/stripe-js"; +import type { StripeError } from "@stripe/stripe-js"; import { usePostResource } from "../services/rest-resource.service"; import { SAVED_STATE_KEY } from "../services/router.service"; +const STRIPE_CODE_MESSAGES: Partial> = { + payment_intent_authentication_failure: + "We were unable to authenticate your payment. Please try a different payment method.", + payment_intent_timeout: "Payment timed out. Please try again.", +}; + +const TECHNICAL_ERROR_TYPES = new Set([ + "api_error", + "invalid_request_error", + "authentication_error", + "rate_limit_error", + "idempotency_error", + "stripe_invalid_response_error", +]); + +const GENERIC_PAYMENT_ERROR = + "An error occurred processing your payment. Please try again."; + +function formatStripeError(error: StripeError): string { + if (error.code && STRIPE_CODE_MESSAGES[error.code]) { + return STRIPE_CODE_MESSAGES[error.code]!; + } + if (!TECHNICAL_ERROR_TYPES.has(error.type)) { + return error.message ?? GENERIC_PAYMENT_ERROR; + } + return GENERIC_PAYMENT_ERROR; +} + export const PaymentDetailsPage: StagerComponent = ({ data, setData, @@ -400,14 +429,19 @@ const StripeForm = ({ }); if (error) { - const message = JSON.stringify(error) - handleError({ message }); + Sentry.captureMessage("Stripe confirmPayment error", { + level: "error", + extra: { stripeError: error }, + }); + setLoading(false); + setErrorMessage(formatStripeError(error)); return; } } catch (e: any) { console.error("Create payment error", e); - handleError({ message: e?.message || JSON.stringify(e) || "Unknown error" }); - Sentry.captureException(e) + Sentry.captureException(e); + setLoading(false); + setErrorMessage(GENERIC_PAYMENT_ERROR); } }; @@ -441,7 +475,7 @@ const StripeForm = ({ setData({ ...data, paymentMethod: stripePaymentMethod === "bacs_debit" ? "directDebit" : "creditCard" }); }}/> - {errorMessage &&
{errorMessage}
} + {errorMessage &&
{errorMessage}
} ); diff --git a/packages/join-flow/src/services/rest-resource.service.ts b/packages/join-flow/src/services/rest-resource.service.ts index d1221e7a..f14d5b30 100644 --- a/packages/join-flow/src/services/rest-resource.service.ts +++ b/packages/join-flow/src/services/rest-resource.service.ts @@ -18,7 +18,11 @@ export const usePostResource = (resource: string) => { }); if (!res.ok) { - throw Error(await res.text()); + const body = await res.text(); + const err = new Error(body) as Error & { status: number; resource: string }; + err.status = res.status; + err.resource = resource; + throw err; } return res.json();