Skip to content

feat(payment): wire PayPal one-time checkout (SDK Buttons + capture)#160

Merged
TortoiseWolfe merged 2 commits into
mainfrom
feat/paypal-onetime-buttons
Jul 3, 2026
Merged

feat(payment): wire PayPal one-time checkout (SDK Buttons + capture)#160
TortoiseWolfe merged 2 commits into
mainfrom
feat/paypal-onetime-buttons

Conversation

@TortoiseWolfe

Copy link
Copy Markdown
Owner

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.

  • 🔴 DEMO-ONLY stub completed: the PayPal button called create-paypal-order then immediately faked success — no PayPal approval popup, no capture. The correct pieces (renderPayPalButtons SDK flow, approvePayPalOrder capture) existed in paypal.ts but had zero callers. Now wired: usePaymentButton.mountPayPalButtons() renders the SDK Buttons (createOrder → PayPal popup → onApprove → capture-paypal-order → onSuccess); PaymentButton mounts them into a per-instance container when the PayPal tab is active + consent granted.
  • 🔴 Prod-config bug fixed: create-paypal-order/capture read the unprefixed PAYPAL_CLIENT_ID, which was never in Vault (only NEXT_PUBLIC_PAYPAL_CLIENT_ID was) — so PayPal orders would 500 in prod. Both name variants now set as fn secrets (same class as the Stripe-session NEXT_PUBLIC_SUPABASE_* fix).
  • ♻️ Intent creation extracted to a shared newIntent(); Stripe path unchanged.

Verification

  • Live backend probe (PayPal sandbox): create-paypal-order returns a real orderId (200) after the Vault fix; CORS ACAO correct.
  • pnpm run type-check clean; prettier clean; 203 payment unit tests green (incl. 24 PaymentButton).
  • In-browser approval leg: requires a human in PayPal's sandbox popup (login → approve), so it's not an unattended E2E — covered by unit tests + the live backend probe. A future manual/semi-automated pass can confirm the popup→capture round-trip.

Notes

  • NEXT_PUBLIC_SITE_URL fn secret is still unset (roadmap follow-up) — success_url/CORS default to localhost; set before live traffic.
  • Subscription lifecycle leg (Session +2) is still unexercised.

🤖 Generated with Claude Code

TortoiseWolfe and others added 2 commits July 3, 2026 10:20
…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>
@TortoiseWolfe

Copy link
Copy Markdown
Owner Author

CI status: sole failure is pre-existing on main, not this PR

PR #160's only red check is E2E (chromium-gen 4/6)
tests/e2e/security/payment-isolation.spec.ts:113 "Payment history shows only own payments". It failed on the initial run and a clean re-run (not flake).

Verified it is NOT caused by this PR:

  • This PR's diff vs origin/main is 4 files, all in the PaymentButton provider-tab/button area (PaymentButton.tsx, usePaymentButton.ts, + their tests). The failing test exercises the PaymentHistory list ([data-payment-item] / "No payment history" heading) — a different component this PR never touches.
  • The current main run (e6861e6) fails the exact same test on all three browsers (chromium + firefox + webkit) with none of this PR's code. So main is red on this test independently.
  • Timing rules out concurrent-backend interference (the mutex serialized the runs; they didn't overlap).

Likely root cause (on main, separate issue): the regression appeared after e6861e6 — #156 "make auth redirects basePath-aware" merged (between the Stripe PR #155 and now). That change touches AuthContext.tsx/redirect logic; payment-isolation depends on a two-user login/session flow, so a redirect regression there would leave the second user's payment history unloaded → the observed empty-render failure. This belongs to the #156 author to fix, not this PR.

Recommendation: do not block #160 on this red — it's a main regression. Merge #160 once main is green again (or via --admin if the team agrees the failure is unrelated + main gets its own fix). Not self-merging here given main is currently red.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants