diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 0531560..eb138ab 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -22,6 +22,7 @@ {"id":"fotospiel-app-5hk","title":"Fix staging coupon seed 500 for E2E","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-03T15:12:53.643644221+01:00","created_by":"soeren","updated_at":"2026-01-03T15:12:53.643644221+01:00"} {"id":"fotospiel-app-5iy","title":"Security review: confirm env/header defaults","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:03:20.808188183+01:00","created_by":"soeren","updated_at":"2026-01-01T16:03:26.388002115+01:00","closed_at":"2026-01-01T16:03:26.388002115+01:00","close_reason":"Completed in codebase (verified)"} {"id":"fotospiel-app-5s3","title":"Localized SEO: canonical/hreflang tags + localized navigation","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:02:03.909947355+01:00","created_by":"soeren","updated_at":"2026-01-01T16:02:09.550647107+01:00","closed_at":"2026-01-01T16:02:09.550647107+01:00","close_reason":"Completed in codebase (verified)"} +{"id":"fotospiel-app-5zl","title":"Ensure checkout step 3 requires login for Paddle checkout","description":"Problem: Paddle checkout on step 3 fails when user is not logged in. Step 3 must enforce authentication before initializing Paddle checkout.\\n\\nSuggestions:\\n- Protect step 3 route/controller with auth middleware and redirect to login with intended return URL.\\n- Gate step 3 UI/CTA on auth state; show inline login prompt and disable Paddle until authenticated.\\n- Require auth in backend endpoint that creates Paddle transaction/session; return 401 and send user to login.\\n- Optionally preflight at end of step 2 to prompt login before advancing.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-04T12:31:43.215017311+01:00","created_by":"soeren","updated_at":"2026-01-04T12:42:45.088723058+01:00","closed_at":"2026-01-04T12:42:45.088723058+01:00","close_reason":"Closed"} {"id":"fotospiel-app-64l","title":"SEC-FE-01 CSP nonce/hashing rollout","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T15:54:47.607047443+01:00","created_by":"soeren","updated_at":"2026-01-01T15:55:56.477104351+01:00","closed_at":"2026-01-01T15:55:56.477104351+01:00","close_reason":"Completed in codebase (verified) - duplicate of fotospiel-app-zli"} {"id":"fotospiel-app-6dp","title":"Coupon ops enhancements (redemption service, preview endpoint, widget, export)","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:09:09.275919717+01:00","created_by":"soeren","updated_at":"2026-01-01T16:09:14.882264149+01:00","closed_at":"2026-01-01T16:09:14.882264149+01:00","close_reason":"Completed in codebase (verified)"} {"id":"fotospiel-app-6oj","title":"Security review: media pipeline code audit (AV/EXIF, signed URLs, storage separation)","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T16:05:31.390878341+01:00","created_by":"soeren","updated_at":"2026-01-01T16:05:31.390878341+01:00"} diff --git a/.beads/last-touched b/.beads/last-touched index 84ce655..73c743b 100644 --- a/.beads/last-touched +++ b/.beads/last-touched @@ -1 +1 @@ -fotospiel-app-99o +fotospiel-app-5zl diff --git a/resources/js/pages/marketing/checkout/CheckoutWizard.tsx b/resources/js/pages/marketing/checkout/CheckoutWizard.tsx index a657d08..8a14d8f 100644 --- a/resources/js/pages/marketing/checkout/CheckoutWizard.tsx +++ b/resources/js/pages/marketing/checkout/CheckoutWizard.tsx @@ -10,6 +10,7 @@ import { AuthStep } from "./steps/AuthStep"; import { ConfirmationStep } from "./steps/ConfirmationStep"; import { useAnalytics } from '@/hooks/useAnalytics'; import { cn } from "@/lib/utils"; +import toast from 'react-hot-toast'; const PaymentStep = lazy(() => import('./steps/PaymentStep').then((module) => ({ default: module.PaymentStep }))); @@ -80,6 +81,7 @@ const WizardBody: React.FC<{ authUser, isAuthenticated, paymentCompleted, + goToStep, } = useCheckoutWizard(); const progressRef = useRef(null); const hasMountedRef = useRef(false); @@ -114,6 +116,13 @@ const WizardBody: React.FC<{ }); }, [currentStep, trackEvent]); + useEffect(() => { + if (currentStep === 'payment' && !isAuthenticated) { + toast.error(t('checkout.payment_step.auth_required')); + goToStep('auth'); + } + }, [currentStep, goToStep, isAuthenticated, t]); + useEffect(() => { if (typeof window === 'undefined' || !progressRef.current) { return; diff --git a/resources/js/pages/marketing/checkout/__tests__/CheckoutWizard.guard.test.tsx b/resources/js/pages/marketing/checkout/__tests__/CheckoutWizard.guard.test.tsx index 9352a3a..e38c878 100644 --- a/resources/js/pages/marketing/checkout/__tests__/CheckoutWizard.guard.test.tsx +++ b/resources/js/pages/marketing/checkout/__tests__/CheckoutWizard.guard.test.tsx @@ -83,6 +83,21 @@ describe('CheckoutWizard auth step navigation guard', () => { nextButtons.forEach((button) => expect(button).not.toBeDisabled()); }); + it('redirects to the auth step when the user is not authenticated on the payment step', async () => { + render( + , + ); + + await screen.findByTestId('auth-step'); + expect(screen.queryByTestId('payment-step')).not.toBeInTheDocument(); + }); + it('only renders the next button on the payment step after the payment is completed', async () => { const paidPackage = { ...basePackage, id: 2, price: 99 }; diff --git a/resources/js/pages/marketing/checkout/steps/PaymentStep.tsx b/resources/js/pages/marketing/checkout/steps/PaymentStep.tsx index d5e894b..7f35336 100644 --- a/resources/js/pages/marketing/checkout/steps/PaymentStep.tsx +++ b/resources/js/pages/marketing/checkout/steps/PaymentStep.tsx @@ -191,6 +191,8 @@ export const PaymentStep: React.FC = () => { nextStep, paddleConfig, authUser, + isAuthenticated, + goToStep, setPaymentCompleted, checkoutSessionId, setCheckoutSessionId, @@ -368,6 +370,13 @@ export const PaymentStep: React.FC = () => { }, [couponCode]); const handleFreeActivation = async () => { + if (!isAuthenticated || !authUser) { + const message = t('checkout.payment_step.auth_required'); + toast.error(message); + goToStep('auth'); + return; + } + if (!selectedPackage) { return; } @@ -420,6 +429,15 @@ export const PaymentStep: React.FC = () => { return; } + if (!isAuthenticated || !authUser) { + const message = t('checkout.payment_step.auth_required'); + setStatus('error'); + setMessage(message); + toast.error(message); + goToStep('auth'); + return; + } + if (!acceptedTerms) { setConsentError(t('checkout.legal.checkbox_terms_error')); return; @@ -466,6 +484,18 @@ export const PaymentStep: React.FC = () => { }); const rawBody = await response.text(); + if ( + response.status === 401 || + response.status === 419 || + (response.redirected && response.url.includes('/login')) + ) { + const message = t('checkout.payment_step.auth_required'); + setStatus('error'); + setMessage(message); + toast.error(message); + goToStep('auth'); + return; + } if (typeof window !== 'undefined') { console.info('[Checkout] Hosted checkout response', { status: response.status, rawBody });