feat(payment): wire PayPal one-time checkout (SDK Buttons + capture)#160
Conversation
…nced to domain root) Auth flow built URLs from window.location.origin and hardcoded '/', ignoring the GitHub Pages basePath. On a project-site deploy the confirmation flow therefore stranded new users at the domain root. - add getInternalUrl/getRedirectUrl to project.config (trailing-slash canonical, degrade to old strings when basePath is empty) - inject the resolved basePath into the client bundle via next.config env (deploy workflows omit NEXT_PUBLIC_BASE_PATH on purpose, so the detected value never reached client code in production) - route emailRedirectTo, OAuth redirectTo, password-reset redirectTo and all hard navigations through the helpers - guard the cross-tab SIGNED_OUT redirect so it can never hijack /auth/callback (transient SIGNED_OUT during confirmation passes the token-validity check because no token exists yet) - pin the seam with unit tests: prefixed emailRedirectTo, prefixed signOut/cross-tab redirects, and no redirect on the callback page Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PayPal one-time was a DEMO-ONLY stub: the button created an order via create-paypal-order then immediately faked success — no PayPal approval UI, no capture. renderPayPalButtons + approvePayPalOrder existed in paypal.ts but were wired into nothing (zero callers). Wire the real flow. usePaymentButton gains mountPayPalButtons(containerId): renderPayPalButtons(createOrder → PayPal approval popup → onApprove → approvePayPalOrder/capture-paypal-order → onSuccess). PaymentButton mounts the SDK Buttons into a per-instance container (useId) when the PayPal tab is active + consent granted, and hides the generic redirect button for that provider. Stripe path unchanged. Intent creation extracted to a shared newIntent() used by both providers. Live-verified the BACKEND end-to-end against the PayPal sandbox: create-paypal-order returns a real orderId (200) — which also surfaced + fixed a prod-config gap: create-paypal-order/capture read the UNPREFIXED PAYPAL_CLIENT_ID, which was never in Vault (only NEXT_PUBLIC_PAYPAL_CLIENT_ID was). Both name variants now set as fn secrets. The in-browser approval leg needs a human in PayPal's popup, so it's covered by unit tests (203 payment tests green) + the live backend probe, not an unattended E2E. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI status: sole failure is pre-existing on
|
Summary
Second payments live-acceptance pass (PayPal one-time). Like the Stripe leg, verifying it exposed that the flow was never actually finished — plus a prod-config bug.
create-paypal-orderthen immediately faked success — no PayPal approval popup, no capture. The correct pieces (renderPayPalButtonsSDK flow,approvePayPalOrdercapture) existed inpaypal.tsbut had zero callers. Now wired:usePaymentButton.mountPayPalButtons()renders the SDK Buttons (createOrder → PayPal popup → onApprove → capture-paypal-order → onSuccess);PaymentButtonmounts them into a per-instance container when the PayPal tab is active + consent granted.create-paypal-order/captureread the unprefixedPAYPAL_CLIENT_ID, which was never in Vault (onlyNEXT_PUBLIC_PAYPAL_CLIENT_IDwas) — so PayPal orders would 500 in prod. Both name variants now set as fn secrets (same class as the Stripe-sessionNEXT_PUBLIC_SUPABASE_*fix).newIntent(); Stripe path unchanged.Verification
create-paypal-orderreturns a realorderId(200) after the Vault fix; CORS ACAO correct.pnpm run type-checkclean; prettier clean; 203 payment unit tests green (incl. 24 PaymentButton).Notes
NEXT_PUBLIC_SITE_URLfn secret is still unset (roadmap follow-up) —success_url/CORS default to localhost; set before live traffic.🤖 Generated with Claude Code