From d87b93df0c47d3cfbaf117c9a4ddfdd176b86c4a Mon Sep 17 00:00:00 2001 From: liudmylasovetovs Date: Thu, 22 Jan 2026 14:46:50 -0800 Subject: [PATCH 01/77] (SP: 1) [Shop] Fix checkout redirect 404 by removing duplicate locale in in-app routes and Stripe return_url --- frontend/app/[locale]/shop/cart/page.tsx | 2 +- .../checkout/payment/StripePaymentClient.tsx | 68 ++++++++++++------- .../shop/checkout/payment/[orderId]/page.tsx | 2 +- .../app/[locale]/shop/orders/[id]/page.tsx | 4 +- frontend/app/[locale]/shop/orders/page.tsx | 2 +- frontend/app/[locale]/shop/products/page.tsx | 2 +- frontend/components/shop/product-sort.tsx | 8 +-- 7 files changed, 55 insertions(+), 33 deletions(-) diff --git a/frontend/app/[locale]/shop/cart/page.tsx b/frontend/app/[locale]/shop/cart/page.tsx index d79fc7b9..5195ec97 100644 --- a/frontend/app/[locale]/shop/cart/page.tsx +++ b/frontend/app/[locale]/shop/cart/page.tsx @@ -19,7 +19,7 @@ export default function CartPage() { const params = useParams<{ locale?: string }>(); const locale = params.locale ?? 'en'; - const shopBase = useMemo(() => `/${locale}/shop`, [locale]); + const shopBase = useMemo(() => `/shop`, []); async function handleCheckout() { setCheckoutError(null); diff --git a/frontend/app/[locale]/shop/checkout/payment/StripePaymentClient.tsx b/frontend/app/[locale]/shop/checkout/payment/StripePaymentClient.tsx index 51fcb73b..7a2131e5 100644 --- a/frontend/app/[locale]/shop/checkout/payment/StripePaymentClient.tsx +++ b/frontend/app/[locale]/shop/checkout/payment/StripePaymentClient.tsx @@ -48,31 +48,55 @@ function toCurrencyCode( : resolveCurrencyFromLocale(locale); } -function buildShopBase(locale: string) { - return `/${locale}/shop`; +/** + * IMPORTANT: + * - In-app navigation uses next-intl routing -> DO NOT prefix locale manually. + * - Stripe return_url is an external redirect -> MUST include locale exactly once. + */ +const IN_APP_SHOP_BASE = '/shop'; + +function normalizeLocale(raw: string): string { + return (raw ?? '').trim().replace(/^\/+/, '').replace(/\/+$/, ''); } -function nextRouteForPaymentResult(params: { +function buildInAppPath(path: string): string { + const p = path.startsWith('/') ? path : `/${path}`; + return `${IN_APP_SHOP_BASE}${p}`; +} + +function buildStripeReturnUrl(params: { locale: string; + inAppPath: string; // must be "/shop/..." +}): string { + const loc = normalizeLocale(params.locale); + const p = params.inAppPath.startsWith('/') + ? params.inAppPath + : `/${params.inAppPath}`; + // Note: p can contain query string; URL() supports it. + return new URL(`/${loc}${p}`, window.location.origin).toString(); +} + +function nextRouteForPaymentResult(params: { orderId: string; status?: string | null; }) { - const { locale, orderId, status } = params; - const shopBase = buildShopBase(locale); + const { orderId, status } = params; const id = encodeURIComponent(orderId); - const success = `${shopBase}/checkout/success?orderId=${id}`; - const failure = `${shopBase}/checkout/error?orderId=${id}`; + const success = buildInAppPath(`/checkout/success?orderId=${id}`); + const failure = buildInAppPath(`/checkout/error?orderId=${id}`); if (!status) return success; if ( status === 'succeeded' || status === 'processing' || status === 'requires_capture' - ) + ) { return success; - if (status === 'requires_payment_method' || status === 'canceled') + } + if (status === 'requires_payment_method' || status === 'canceled') { return failure; + } return success; } @@ -81,8 +105,6 @@ function StripePaymentForm({ orderId, locale }: PaymentFormProps) { const elements = useElements(); const router = useRouter(); - const shopBase = useMemo(() => buildShopBase(locale), [locale]); - const [submitting, setSubmitting] = useState(false); const [errorMessage, setErrorMessage] = useState(null); @@ -102,23 +124,25 @@ function StripePaymentForm({ orderId, locale }: PaymentFormProps) { try { const id = encodeURIComponent(orderId); + const inAppSuccess = buildInAppPath(`/checkout/success?orderId=${id}`); + const inAppFailure = buildInAppPath(`/checkout/error?orderId=${id}`); + const { error, paymentIntent } = await stripe.confirmPayment({ elements, redirect: 'if_required', confirmParams: { - // Stripe redirect comes from outside Next.js routing — must include locale. - return_url: `${window.location.origin}${shopBase}/checkout/success?orderId=${id}`, + // Stripe redirect comes from outside Next.js routing — MUST include locale exactly once. + return_url: buildStripeReturnUrl({ locale, inAppPath: inAppSuccess }), }, }); if (error) { setErrorMessage(error.message ?? 'Unable to confirm payment.'); - router.push(`${shopBase}/checkout/error?orderId=${id}`); + router.push(inAppFailure); return; } const next = nextRouteForPaymentResult({ - locale, orderId, status: paymentIntent?.status ?? null, }); @@ -128,7 +152,7 @@ function StripePaymentForm({ orderId, locale }: PaymentFormProps) { logError('stripe_payment_confirm_failed', error, { orderId }); setErrorMessage('We couldn’t confirm your payment. Please try again.'); router.push( - `${shopBase}/checkout/error?orderId=${encodeURIComponent(orderId)}` + buildInAppPath(`/checkout/error?orderId=${encodeURIComponent(orderId)}`) ); } finally { setSubmitting(false); @@ -175,8 +199,6 @@ export default function StripePaymentClient({ [currency, locale] ); - const shopBase = useMemo(() => buildShopBase(locale), [locale]); - const stripePromise = useMemo(() => { if (!paymentsEnabled || !publishableKey) return null; return loadStripe(publishableKey); @@ -199,15 +221,15 @@ export default function StripePaymentClient({

Payments are disabled in this environment.