@
All checks were successful
Deploy to Production / deploy (push) Successful in 1m22s

feat(billing): in-app embedded Stripe checkout + webhook hardening

Checkout previously used hosted ui_mode → window.location to checkout.stripe.com,
which pops out of the installed PWA into the system browser. Switch to embedded:

- API: ui_mode embedded_page (stripe-node v22 / API 2025-10 renamed the enum),
  return_url instead of success/cancel_url, returns client_secret.
- web: @stripe/react-stripe-js EmbeddedCheckout mounted in an in-app modal;
  NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY baked at build (Dockerfile arg + compose arg).
- .env.production.example: full Stripe section (was missing) + admin-email
  placeholder (INF-001).

Also bundled (same files): BILL-002 invoice.paid resets quota only on
subscription_cycle; BILL-003 webhook dedup rolled back on handler failure;
BILL-001 change-plan writes plan locally; BILL-004 webhook cross-checks
sub.customer before trusting metadata.orgId; INF-003 API routed off the raw
docker.sock through a locked-down tecnativa/docker-socket-proxy (CONTAINERS+POST).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
This commit is contained in:
Marco Sadjadi
2026-05-29 20:56:40 +02:00
parent 9d5386ccba
commit cf423de3d5
8 changed files with 256 additions and 20 deletions

53
pnpm-lock.yaml generated
View File

@@ -137,6 +137,12 @@ importers:
'@bmm/types':
specifier: workspace:*
version: link:../../packages/types
'@stripe/react-stripe-js':
specifier: ^6.4.0
version: 6.4.0(@stripe/stripe-js@9.7.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@stripe/stripe-js':
specifier: ^9.7.0
version: 9.7.0
clsx:
specifier: 2.1.1
version: 2.1.1
@@ -1244,6 +1250,17 @@ packages:
'@remotion/studio@4.0.220':
resolution: {integrity: sha512-97sb8ta+4cRmsntAWPgyNkAGo3i9/q/j+jLBvhbUF0njC+rFs3QVb/qx27mEI8/Avw3har7Nu7BR62eH8TNfcg==}
'@stripe/react-stripe-js@6.4.0':
resolution: {integrity: sha512-5cAf7GAqf8VHPoPLVnJQ2MHtDWLdiEPs5GW/loU7iDEue3xf620v11skrk5HICUraDOhODPYpttgFC5N579NxA==}
peerDependencies:
'@stripe/stripe-js': '>=9.5.0 <10.0.0'
react: '>=16.8.0 <20.0.0'
react-dom: '>=16.8.0 <20.0.0'
'@stripe/stripe-js@9.7.0':
resolution: {integrity: sha512-r1ElolvWXM4aYnZZVHvKW3EDL8JcwEuIgTuWxlB5lvC+YsvjkQ0gX35x9d8dTDubX395fViLVqkaolVs1PmIQQ==}
engines: {node: '>=12.16'}
'@swc/counter@0.1.3':
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
@@ -2103,6 +2120,9 @@ packages:
jose@6.2.3:
resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
@@ -2217,6 +2237,10 @@ packages:
lodash.sortby@4.7.0:
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
@@ -2471,6 +2495,9 @@ packages:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
@@ -2502,6 +2529,9 @@ packages:
peerDependencies:
react: ^19.0.0
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
react-refresh@0.9.0:
resolution: {integrity: sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==}
engines: {node: '>=0.10.0'}
@@ -3703,6 +3733,15 @@ snapshots:
- supports-color
- utf-8-validate
'@stripe/react-stripe-js@6.4.0(@stripe/stripe-js@9.7.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@stripe/stripe-js': 9.7.0
prop-types: 15.8.1
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
'@stripe/stripe-js@9.7.0': {}
'@swc/counter@0.1.3': {}
'@swc/helpers@0.5.15':
@@ -4607,6 +4646,8 @@ snapshots:
jose@6.2.3: {}
js-tokens@4.0.0: {}
json-parse-even-better-errors@2.3.1: {}
json-schema-ref-resolver@3.0.0:
@@ -4692,6 +4733,10 @@ snapshots:
lodash.sortby@4.7.0: {}
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
lru-cache@6.0.0:
dependencies:
yallist: 4.0.0
@@ -4919,6 +4964,12 @@ snapshots:
kleur: 3.0.3
sisteransi: 1.0.5
prop-types@15.8.1:
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
react-is: 16.13.1
proxy-addr@2.0.7:
dependencies:
forwarded: 0.2.0
@@ -4951,6 +5002,8 @@ snapshots:
react: 19.0.0
scheduler: 0.25.0
react-is@16.13.1: {}
react-refresh@0.9.0: {}
react@19.0.0: {}