@aglyn/plugins-commerce 1.0.0-beta.216 → 1.0.0-beta.218
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -15
- package/src/lib/components/storefront-payment-element.js +36 -5
- package/src/lib/components/storefront-payment-element.js.map +1 -1
- package/src/lib/server/native-checkout.d.ts +20 -5
- package/src/lib/server/native-checkout.js +27 -6
- package/src/lib/server/native-checkout.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-commerce",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.218",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,20 +25,20 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
37
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
40
|
-
"@stripe/react-stripe-js": "^
|
|
41
|
-
"@stripe/stripe-js": "^
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.218",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.218",
|
|
30
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.218",
|
|
31
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.218",
|
|
32
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.218",
|
|
33
|
+
"@aglyn/shared-util-email": "1.0.0-beta.218",
|
|
34
|
+
"@aglyn/shared-util-http": "1.0.0-beta.218",
|
|
35
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.218",
|
|
36
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.218",
|
|
37
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.218",
|
|
38
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.218",
|
|
39
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.218",
|
|
40
|
+
"@stripe/react-stripe-js": "^6.12.0",
|
|
41
|
+
"@stripe/stripe-js": "^9.17.0",
|
|
42
42
|
"@swc/helpers": "0.5.23",
|
|
43
43
|
"qrcode.react": "^4.2.0"
|
|
44
44
|
},
|
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
-
|
|
18
|
+
// The `/checkout` entry, not the main one: react-stripe-js v6 moved the
|
|
19
|
+
// Checkout Sessions provider, its hook and its own `PaymentElement` there, and
|
|
20
|
+
// the main entry's `PaymentElement` is the Elements one, which does not mount
|
|
21
|
+
// under a Checkout provider.
|
|
22
|
+
import { CheckoutElementsProvider, PaymentElement, useCheckoutElements } from "@stripe/react-stripe-js/checkout";
|
|
19
23
|
// `/pure` deliberately, not the bare entry (AGL-2486). The bare entry ends
|
|
20
24
|
// with a module-scope `Promise.resolve().then(() => getStripePromise())`,
|
|
21
25
|
// so merely EVALUATING it injects the js.stripe.com/v3 script tag —
|
|
@@ -45,14 +49,17 @@ function getStripe(publishableKey) {
|
|
|
45
49
|
stripePromises.clear();
|
|
46
50
|
}
|
|
47
51
|
function PaymentForm({ payLabel, onCancel }) {
|
|
48
|
-
|
|
52
|
+
// A `{ type: 'loading' | 'success' | 'error' }` union since react-stripe-js
|
|
53
|
+
// v6: the session is not usable until Stripe.js has booted against it.
|
|
54
|
+
const checkoutState = useCheckoutElements();
|
|
55
|
+
const checkout = checkoutState.type === 'success' ? checkoutState.checkout : null;
|
|
49
56
|
const [status, setStatus] = useState('idle');
|
|
50
57
|
const [message, setMessage] = useState('');
|
|
51
58
|
const handlePay = useCallback(async ()=>{
|
|
52
59
|
// Guarded rather than merely disabled: a disabled button is a rendering
|
|
53
60
|
// fact, and a keyboard or a slow re-render can still land a second call
|
|
54
61
|
// during a 3DS challenge.
|
|
55
|
-
if (status === 'confirming') return;
|
|
62
|
+
if (status === 'confirming' || !checkout) return;
|
|
56
63
|
setStatus('confirming');
|
|
57
64
|
setMessage('');
|
|
58
65
|
try {
|
|
@@ -78,6 +85,30 @@ function PaymentForm({ payLabel, onCancel }) {
|
|
|
78
85
|
checkout,
|
|
79
86
|
status
|
|
80
87
|
]);
|
|
88
|
+
if (checkoutState.type === 'error') {
|
|
89
|
+
// The session could not be loaded — expired, or already completed in
|
|
90
|
+
// another tab. Nothing typed here could be charged, so say so and offer
|
|
91
|
+
// the way back rather than a form that cannot submit.
|
|
92
|
+
return /*#__PURE__*/ _jsxs(Box, {
|
|
93
|
+
sx: {
|
|
94
|
+
display: 'flex',
|
|
95
|
+
flexDirection: 'column',
|
|
96
|
+
gap: 2
|
|
97
|
+
},
|
|
98
|
+
children: [
|
|
99
|
+
/*#__PURE__*/ _jsx(Alert, {
|
|
100
|
+
severity: "error",
|
|
101
|
+
role: "alert",
|
|
102
|
+
children: 'This checkout could not be loaded. Return to the store and try again.'
|
|
103
|
+
}),
|
|
104
|
+
onCancel ? /*#__PURE__*/ _jsx(Button, {
|
|
105
|
+
variant: "text",
|
|
106
|
+
onClick: onCancel,
|
|
107
|
+
children: 'Back to the store'
|
|
108
|
+
}) : null
|
|
109
|
+
]
|
|
110
|
+
});
|
|
111
|
+
}
|
|
81
112
|
return /*#__PURE__*/ _jsxs(Box, {
|
|
82
113
|
sx: {
|
|
83
114
|
display: 'flex',
|
|
@@ -103,7 +134,7 @@ function PaymentForm({ payLabel, onCancel }) {
|
|
|
103
134
|
/*#__PURE__*/ _jsx(Button, {
|
|
104
135
|
variant: "contained",
|
|
105
136
|
onClick: handlePay,
|
|
106
|
-
disabled: status === 'confirming',
|
|
137
|
+
disabled: status === 'confirming' || !checkout,
|
|
107
138
|
fullWidth: true,
|
|
108
139
|
children: status === 'confirming' ? 'Paying…' : payLabel || 'Pay now'
|
|
109
140
|
}),
|
|
@@ -126,7 +157,7 @@ export function StorefrontPaymentElement({ clientSecret, publishableKey, payLabe
|
|
|
126
157
|
// mode without a publishable key: rendering nothing beats rendering an empty
|
|
127
158
|
// white box that looks like a broken payment form.
|
|
128
159
|
if (!clientSecret || !stripe) return null;
|
|
129
|
-
return /*#__PURE__*/ _jsx(
|
|
160
|
+
return /*#__PURE__*/ _jsx(CheckoutElementsProvider, {
|
|
130
161
|
stripe: stripe,
|
|
131
162
|
options: {
|
|
132
163
|
clientSecret
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/components/storefront-payment-element.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport { CheckoutProvider, PaymentElement, useCheckout } from '@stripe/react-stripe-js'\n// `/pure` deliberately, not the bare entry (AGL-2486). The bare entry ends\n// with a module-scope `Promise.resolve().then(() => getStripePromise())`,\n// so merely EVALUATING it injects the js.stripe.com/v3 script tag —\n// 249 KB gzipped — on any page that reaches this module, whether or not\n// `loadStripe` is ever called. `/pure` is the same API without that\n// statement, so the script is fetched when we ask for it and not before.\nimport { loadStripe } from '@stripe/stripe-js/pure'\n// The TYPE only, and from the bare entry because `/pure` does not re-export\n// it. `import type` is erased entirely at compile time, so this adds no\n// runtime import and cannot re-introduce the script injection above.\nimport type { Stripe } from '@stripe/stripe-js'\nimport Alert from '@mui/material/Alert'\nimport Box from '@mui/material/Box'\nimport Button from '@mui/material/Button'\nimport { useCallback, useMemo, useState } from 'react'\n\n/**\n * The storefront Payment Element (AGL-1944) — the shopper pays on the\n * merchant's own domain instead of being sent to `checkout.stripe.com`.\n *\n * ## THIS COMPONENT DOES NOT FULFIL ANYTHING\n *\n * It has no success callback, and that absence is the design rather than an\n * omission. `libs/plugins/commerce/src/lib/server/billing-webhook.ts` creates\n * the order on `checkout.session.completed` and is the only thing that ever\n * does. An in-page flow makes trusting the browser far more tempting than a\n * redirect does — the form is right there, `confirm()` resolves in hand, and\n * posting \"done, please fulfil\" is one line away — and the browser is the one\n * participant in a payment that can lie about having paid.\n *\n * It is also the participant that can VANISH. A shopper who confirms and\n * closes the tab never runs a callback, so a flow that fulfils here loses the\n * order outright while keeping the money; a shopper who refreshes the return\n * page runs it twice. Neither is reachable from a design with no callback in\n * it. What happens after a successful confirm is a redirect to `return_url`,\n * carrying the `session_id` the server put there — so the page can SHOW the\n * shopper a result by looking the order up, without being TOLD the outcome by\n * this code.\n *\n * ## Declines and 3DS\n *\n * Both were handled for us by the redirect and now have to be handled here.\n *\n * A decline resolves as `{ type: 'error' }` and is rendered inline WITHOUT\n * unmounting the form: the shopper's basket, address and chosen method are all\n * still on screen and they can try another card. That is the whole difference\n * between a recoverable decline and a dead end, and it is why the error state\n * below sits beside the element rather than replacing it.\n *\n * A 3DS challenge is Stripe's own: `confirm()` presents the challenge and, for\n * redirect-based methods, sends the browser to `return_url` and back. Nothing\n * here has to model it — but the button must stay disabled across the whole\n * await, because a second `confirm()` during a challenge is exactly the\n * double-submit the server's idempotency claim exists to survive and there is\n * no reason to make it lean on that.\n */\n\nexport interface StorefrontPaymentElementProps {\n /** Stripe's instruction to the browser. Not a claim that money moved. */\n clientSecret: string\n publishableKey: string\n /** Shown on the pay button, so the merchant's own total reads through. */\n payLabel?: string\n /** Abandon the in-page form and return to the store. */\n onCancel?: () => void\n}\n\n/**\n * `loadStripe` injects a script tag, so it is memoized per key rather than\n * called per render. Keyed because a page could in principle mount this for\n * two different merchants; in practice it is one, and the map costs nothing.\n */\nconst stripePromises = new Map<string, Promise<Stripe | null>>()\nfunction getStripe(publishableKey: string): Promise<Stripe | null> | null {\n if (!publishableKey) return null\n let promise = stripePromises.get(publishableKey)\n if (!promise) {\n promise = loadStripe(publishableKey)\n stripePromises.set(publishableKey, promise)\n }\n return promise\n}\n\n/** Test seam: `loadStripe` caches per key across a file's tests otherwise. */\nexport function __resetStripePromises(): void {\n stripePromises.clear()\n}\n\nfunction PaymentForm({\n payLabel,\n onCancel,\n}: Pick<StorefrontPaymentElementProps, 'payLabel' | 'onCancel'>) {\n const checkout = useCheckout()\n const [status, setStatus] = useState<'idle' | 'confirming' | 'error'>('idle')\n const [message, setMessage] = useState('')\n\n const handlePay = useCallback(async () => {\n // Guarded rather than merely disabled: a disabled button is a rendering\n // fact, and a keyboard or a slow re-render can still land a second call\n // during a 3DS challenge.\n if (status === 'confirming') return\n setStatus('confirming')\n setMessage('')\n try {\n const result = await checkout.confirm()\n if (result?.type === 'error') {\n // A decline, an expired card, an incomplete field. Recoverable: the\n // form stays mounted with everything the shopper typed still in it.\n setMessage(\n String(result.error?.message ?? '') ||\n 'That payment could not be completed. Try another card.',\n )\n setStatus('error')\n return\n }\n // Success does NOT mean fulfilled, and nothing is reported to our server\n // here. Stripe redirects to the `return_url` the session carries; if it\n // has not by the time this resolves, the confirm is still in flight and\n // the button stays disabled rather than inviting a second attempt.\n } catch {\n setMessage('That payment could not be completed. Try another card.')\n setStatus('error')\n }\n }, [checkout, status])\n\n return (\n <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <PaymentElement />\n {message ? (\n // `error` severity is right here and wrong for a lockdown pause — this\n // really is \"your payment did not go through\", which is the one thing\n // a shopper must be told plainly.\n <Alert severity=\"error\" role=\"alert\">\n {message}\n </Alert>\n ) : null}\n <Box sx={{ display: 'flex', gap: 1 }}>\n <Button\n variant=\"contained\"\n onClick={handlePay}\n disabled={status === 'confirming'}\n fullWidth\n >\n {status === 'confirming' ? 'Paying…' : payLabel || 'Pay now'}\n </Button>\n {onCancel ? (\n <Button\n variant=\"text\"\n onClick={onCancel}\n disabled={status === 'confirming'}\n >\n {'Cancel'}\n </Button>\n ) : null}\n </Box>\n </Box>\n )\n}\n\nexport function StorefrontPaymentElement({\n clientSecret,\n publishableKey,\n payLabel,\n onCancel,\n}: StorefrontPaymentElementProps) {\n const stripe = useMemo(() => getStripe(publishableKey), [publishableKey])\n // Belt and braces with the server's own gate, which already refuses native\n // mode without a publishable key: rendering nothing beats rendering an empty\n // white box that looks like a broken payment form.\n if (!clientSecret || !stripe) return null\n return (\n <CheckoutProvider stripe={stripe} options={{ clientSecret }}>\n <Box sx={{ py: 2 }} data-testid=\"storefront-payment-element\">\n <PaymentForm payLabel={payLabel} onCancel={onCancel} />\n </Box>\n </CheckoutProvider>\n )\n}\n\n/**\n * Re-exported so the old import path still resolves, but the definition now\n * lives in its own module (AGL-2486): a fallback imported FROM the lazily\n * loaded module drags that module into the eager bundle and cancels the\n * `lazy()`. Callers must import it from `./storefront-payment-element-fallback`\n * — importing it from here is exactly the bug, and re-exporting it does not\n * make that safe.\n */\nexport { StorefrontPaymentElementFallback } from './storefront-payment-element-fallback'\n\nexport default StorefrontPaymentElement\n"],"names":["CheckoutProvider","PaymentElement","useCheckout","loadStripe","Alert","Box","Button","useCallback","useMemo","useState","stripePromises","Map","getStripe","publishableKey","promise","get","set","__resetStripePromises","clear","PaymentForm","payLabel","onCancel","checkout","status","setStatus","message","setMessage","handlePay","result","confirm","type","String","error","sx","display","flexDirection","gap","severity","role","variant","onClick","disabled","fullWidth","StorefrontPaymentElement","clientSecret","stripe","options","py","data-testid","StorefrontPaymentElementFallback"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/components/storefront-payment-element.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\n// The `/checkout` entry, not the main one: react-stripe-js v6 moved the\n// Checkout Sessions provider, its hook and its own `PaymentElement` there, and\n// the main entry's `PaymentElement` is the Elements one, which does not mount\n// under a Checkout provider.\nimport {\n CheckoutElementsProvider,\n PaymentElement,\n useCheckoutElements,\n} from '@stripe/react-stripe-js/checkout'\n// `/pure` deliberately, not the bare entry (AGL-2486). The bare entry ends\n// with a module-scope `Promise.resolve().then(() => getStripePromise())`,\n// so merely EVALUATING it injects the js.stripe.com/v3 script tag —\n// 249 KB gzipped — on any page that reaches this module, whether or not\n// `loadStripe` is ever called. `/pure` is the same API without that\n// statement, so the script is fetched when we ask for it and not before.\nimport { loadStripe } from '@stripe/stripe-js/pure'\n// The TYPE only, and from the bare entry because `/pure` does not re-export\n// it. `import type` is erased entirely at compile time, so this adds no\n// runtime import and cannot re-introduce the script injection above.\nimport type { Stripe } from '@stripe/stripe-js'\nimport Alert from '@mui/material/Alert'\nimport Box from '@mui/material/Box'\nimport Button from '@mui/material/Button'\nimport { useCallback, useMemo, useState } from 'react'\n\n/**\n * The storefront Payment Element (AGL-1944) — the shopper pays on the\n * merchant's own domain instead of being sent to `checkout.stripe.com`.\n *\n * ## THIS COMPONENT DOES NOT FULFIL ANYTHING\n *\n * It has no success callback, and that absence is the design rather than an\n * omission. `libs/plugins/commerce/src/lib/server/billing-webhook.ts` creates\n * the order on `checkout.session.completed` and is the only thing that ever\n * does. An in-page flow makes trusting the browser far more tempting than a\n * redirect does — the form is right there, `confirm()` resolves in hand, and\n * posting \"done, please fulfil\" is one line away — and the browser is the one\n * participant in a payment that can lie about having paid.\n *\n * It is also the participant that can VANISH. A shopper who confirms and\n * closes the tab never runs a callback, so a flow that fulfils here loses the\n * order outright while keeping the money; a shopper who refreshes the return\n * page runs it twice. Neither is reachable from a design with no callback in\n * it. What happens after a successful confirm is a redirect to `return_url`,\n * carrying the `session_id` the server put there — so the page can SHOW the\n * shopper a result by looking the order up, without being TOLD the outcome by\n * this code.\n *\n * ## Declines and 3DS\n *\n * Both were handled for us by the redirect and now have to be handled here.\n *\n * A decline resolves as `{ type: 'error' }` and is rendered inline WITHOUT\n * unmounting the form: the shopper's basket, address and chosen method are all\n * still on screen and they can try another card. That is the whole difference\n * between a recoverable decline and a dead end, and it is why the error state\n * below sits beside the element rather than replacing it.\n *\n * A 3DS challenge is Stripe's own: `confirm()` presents the challenge and, for\n * redirect-based methods, sends the browser to `return_url` and back. Nothing\n * here has to model it — but the button must stay disabled across the whole\n * await, because a second `confirm()` during a challenge is exactly the\n * double-submit the server's idempotency claim exists to survive and there is\n * no reason to make it lean on that.\n */\n\nexport interface StorefrontPaymentElementProps {\n /** Stripe's instruction to the browser. Not a claim that money moved. */\n clientSecret: string\n publishableKey: string\n /** Shown on the pay button, so the merchant's own total reads through. */\n payLabel?: string\n /** Abandon the in-page form and return to the store. */\n onCancel?: () => void\n}\n\n/**\n * `loadStripe` injects a script tag, so it is memoized per key rather than\n * called per render. Keyed because a page could in principle mount this for\n * two different merchants; in practice it is one, and the map costs nothing.\n */\nconst stripePromises = new Map<string, Promise<Stripe | null>>()\nfunction getStripe(publishableKey: string): Promise<Stripe | null> | null {\n if (!publishableKey) return null\n let promise = stripePromises.get(publishableKey)\n if (!promise) {\n promise = loadStripe(publishableKey)\n stripePromises.set(publishableKey, promise)\n }\n return promise\n}\n\n/** Test seam: `loadStripe` caches per key across a file's tests otherwise. */\nexport function __resetStripePromises(): void {\n stripePromises.clear()\n}\n\nfunction PaymentForm({\n payLabel,\n onCancel,\n}: Pick<StorefrontPaymentElementProps, 'payLabel' | 'onCancel'>) {\n // A `{ type: 'loading' | 'success' | 'error' }` union since react-stripe-js\n // v6: the session is not usable until Stripe.js has booted against it.\n const checkoutState = useCheckoutElements()\n const checkout =\n checkoutState.type === 'success' ? checkoutState.checkout : null\n const [status, setStatus] = useState<'idle' | 'confirming' | 'error'>('idle')\n const [message, setMessage] = useState('')\n\n const handlePay = useCallback(async () => {\n // Guarded rather than merely disabled: a disabled button is a rendering\n // fact, and a keyboard or a slow re-render can still land a second call\n // during a 3DS challenge.\n if (status === 'confirming' || !checkout) return\n setStatus('confirming')\n setMessage('')\n try {\n const result = await checkout.confirm()\n if (result?.type === 'error') {\n // A decline, an expired card, an incomplete field. Recoverable: the\n // form stays mounted with everything the shopper typed still in it.\n setMessage(\n String(result.error?.message ?? '') ||\n 'That payment could not be completed. Try another card.',\n )\n setStatus('error')\n return\n }\n // Success does NOT mean fulfilled, and nothing is reported to our server\n // here. Stripe redirects to the `return_url` the session carries; if it\n // has not by the time this resolves, the confirm is still in flight and\n // the button stays disabled rather than inviting a second attempt.\n } catch {\n setMessage('That payment could not be completed. Try another card.')\n setStatus('error')\n }\n }, [checkout, status])\n\n if (checkoutState.type === 'error') {\n // The session could not be loaded — expired, or already completed in\n // another tab. Nothing typed here could be charged, so say so and offer\n // the way back rather than a form that cannot submit.\n return (\n <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <Alert severity=\"error\" role=\"alert\">\n {'This checkout could not be loaded. Return to the store and try again.'}\n </Alert>\n {onCancel ? (\n <Button variant=\"text\" onClick={onCancel}>\n {'Back to the store'}\n </Button>\n ) : null}\n </Box>\n )\n }\n\n return (\n <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <PaymentElement />\n {message ? (\n // `error` severity is right here and wrong for a lockdown pause — this\n // really is \"your payment did not go through\", which is the one thing\n // a shopper must be told plainly.\n <Alert severity=\"error\" role=\"alert\">\n {message}\n </Alert>\n ) : null}\n <Box sx={{ display: 'flex', gap: 1 }}>\n <Button\n variant=\"contained\"\n onClick={handlePay}\n disabled={status === 'confirming' || !checkout}\n fullWidth\n >\n {status === 'confirming' ? 'Paying…' : payLabel || 'Pay now'}\n </Button>\n {onCancel ? (\n <Button\n variant=\"text\"\n onClick={onCancel}\n disabled={status === 'confirming'}\n >\n {'Cancel'}\n </Button>\n ) : null}\n </Box>\n </Box>\n )\n}\n\nexport function StorefrontPaymentElement({\n clientSecret,\n publishableKey,\n payLabel,\n onCancel,\n}: StorefrontPaymentElementProps) {\n const stripe = useMemo(() => getStripe(publishableKey), [publishableKey])\n // Belt and braces with the server's own gate, which already refuses native\n // mode without a publishable key: rendering nothing beats rendering an empty\n // white box that looks like a broken payment form.\n if (!clientSecret || !stripe) return null\n return (\n <CheckoutElementsProvider stripe={stripe} options={{ clientSecret }}>\n <Box sx={{ py: 2 }} data-testid=\"storefront-payment-element\">\n <PaymentForm payLabel={payLabel} onCancel={onCancel} />\n </Box>\n </CheckoutElementsProvider>\n )\n}\n\n/**\n * Re-exported so the old import path still resolves, but the definition now\n * lives in its own module (AGL-2486): a fallback imported FROM the lazily\n * loaded module drags that module into the eager bundle and cancels the\n * `lazy()`. Callers must import it from `./storefront-payment-element-fallback`\n * — importing it from here is exactly the bug, and re-exporting it does not\n * make that safe.\n */\nexport { StorefrontPaymentElementFallback } from './storefront-payment-element-fallback'\n\nexport default StorefrontPaymentElement\n"],"names":["CheckoutElementsProvider","PaymentElement","useCheckoutElements","loadStripe","Alert","Box","Button","useCallback","useMemo","useState","stripePromises","Map","getStripe","publishableKey","promise","get","set","__resetStripePromises","clear","PaymentForm","payLabel","onCancel","checkoutState","checkout","type","status","setStatus","message","setMessage","handlePay","result","confirm","String","error","sx","display","flexDirection","gap","severity","role","variant","onClick","disabled","fullWidth","StorefrontPaymentElement","clientSecret","stripe","options","py","data-testid","StorefrontPaymentElementFallback"],"mappings":"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"}
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
* subscriptions — would need a parallel implementation reading a different
|
|
38
38
|
* object shape.
|
|
39
39
|
*
|
|
40
|
-
* `ui_mode: '
|
|
40
|
+
* `ui_mode: 'elements'` is the Payment Element backed by a Checkout Session
|
|
41
|
+
* (`'custom'` before the 2026-03-25.dahlia API renamed it). The
|
|
41
42
|
* shopper sees `<PaymentElement/>` inside the merchant's own layout — no Stripe
|
|
42
43
|
* chrome, which is the thing AGL-1944 objected to about embedded Checkout — and
|
|
43
44
|
* the server keeps the session builder it already has. So:
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
*
|
|
51
52
|
* ## The API version, and why it is pinned per-request
|
|
52
53
|
*
|
|
53
|
-
* Measured against live Stripe in test mode: `ui_mode: 'custom'`
|
|
54
|
+
* Measured against live Stripe in test mode: `ui_mode: 'custom'` was refused at
|
|
54
55
|
* the platform account's default API version with
|
|
55
56
|
*
|
|
56
57
|
* Invalid Stripe API version: 2020-08-27. In order to use `ui_mode:
|
|
@@ -65,6 +66,20 @@
|
|
|
65
66
|
* pinned response is `client_secret` and `id`, neither of which the version
|
|
66
67
|
* affects.
|
|
67
68
|
*
|
|
69
|
+
* ### Dahlia (AGL-3410)
|
|
70
|
+
*
|
|
71
|
+
* The pin moved from `2025-03-31.basil` to `2026-03-25.dahlia` with the
|
|
72
|
+
* Stripe.js 9 / react-stripe-js 6 upgrade, because the browser half now boots
|
|
73
|
+
* Stripe.js's Dahlia release train, and Dahlia renamed the mode this module
|
|
74
|
+
* sets: `custom` is refused outright and `elements` replaces it. Across the two
|
|
75
|
+
* releases crossed (Clover, Dahlia) the only other Checkout-session break that
|
|
76
|
+
* reaches a param built here is Clover's switch of new subscriptions to
|
|
77
|
+
* FLEXIBLE billing mode — so a subscription session pins
|
|
78
|
+
* `billing_mode[type]=classic` below, and the subscription it creates bills
|
|
79
|
+
* exactly as the hosted path's does. The measurement below was taken at
|
|
80
|
+
* basil; re-take it in test mode before `release_native_checkout` is turned
|
|
81
|
+
* on anywhere.
|
|
82
|
+
*
|
|
68
83
|
* Same probe, same params (line items, a manual tax line,
|
|
69
84
|
* `shipping_address_collection`, a `fixed_amount` shipping option, Connect
|
|
70
85
|
* `transfer_data[destination]` and `application_fee_amount`), hosted vs custom:
|
|
@@ -82,10 +97,10 @@
|
|
|
82
97
|
* browser is the one participant that can lie about having paid.
|
|
83
98
|
*/
|
|
84
99
|
/**
|
|
85
|
-
* The API version `ui_mode: '
|
|
100
|
+
* The API version `ui_mode: 'elements'` needs, pinned on the session-create
|
|
86
101
|
* request only. Stated once so the header and the doc comment cannot drift.
|
|
87
102
|
*/
|
|
88
|
-
export declare const NATIVE_CHECKOUT_STRIPE_VERSION = "
|
|
103
|
+
export declare const NATIVE_CHECKOUT_STRIPE_VERSION = "2026-03-25.dahlia";
|
|
89
104
|
/** The flag both halves of native checkout share (AGL-1132, AGL-1944). */
|
|
90
105
|
export declare const NATIVE_CHECKOUT_FLAG: "release_native_checkout";
|
|
91
106
|
export interface NativeCheckoutMode {
|
|
@@ -130,7 +145,7 @@ export declare function resolveNativeCheckoutMode(orgId: string | null | undefin
|
|
|
130
145
|
* changing what the shopper is charged.
|
|
131
146
|
*
|
|
132
147
|
* `returnUrl` is the caller's OWN `success_url` string, `{CHECKOUT_SESSION_ID}`
|
|
133
|
-
* placeholder and all. Stripe substitutes it on
|
|
148
|
+
* placeholder and all. Stripe substitutes it on an embedded-components return
|
|
134
149
|
* exactly as it does on a hosted redirect (measured), so the storefront's
|
|
135
150
|
* post-purchase reporting — which reads `session_id` off the query to name the
|
|
136
151
|
* order it just completed (AGL-1641) — keeps working unchanged.
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
* subscriptions — would need a parallel implementation reading a different
|
|
38
38
|
* object shape.
|
|
39
39
|
*
|
|
40
|
-
* `ui_mode: '
|
|
40
|
+
* `ui_mode: 'elements'` is the Payment Element backed by a Checkout Session
|
|
41
|
+
* (`'custom'` before the 2026-03-25.dahlia API renamed it). The
|
|
41
42
|
* shopper sees `<PaymentElement/>` inside the merchant's own layout — no Stripe
|
|
42
43
|
* chrome, which is the thing AGL-1944 objected to about embedded Checkout — and
|
|
43
44
|
* the server keeps the session builder it already has. So:
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
*
|
|
51
52
|
* ## The API version, and why it is pinned per-request
|
|
52
53
|
*
|
|
53
|
-
* Measured against live Stripe in test mode: `ui_mode: 'custom'`
|
|
54
|
+
* Measured against live Stripe in test mode: `ui_mode: 'custom'` was refused at
|
|
54
55
|
* the platform account's default API version with
|
|
55
56
|
*
|
|
56
57
|
* Invalid Stripe API version: 2020-08-27. In order to use `ui_mode:
|
|
@@ -65,6 +66,20 @@
|
|
|
65
66
|
* pinned response is `client_secret` and `id`, neither of which the version
|
|
66
67
|
* affects.
|
|
67
68
|
*
|
|
69
|
+
* ### Dahlia (AGL-3410)
|
|
70
|
+
*
|
|
71
|
+
* The pin moved from `2025-03-31.basil` to `2026-03-25.dahlia` with the
|
|
72
|
+
* Stripe.js 9 / react-stripe-js 6 upgrade, because the browser half now boots
|
|
73
|
+
* Stripe.js's Dahlia release train, and Dahlia renamed the mode this module
|
|
74
|
+
* sets: `custom` is refused outright and `elements` replaces it. Across the two
|
|
75
|
+
* releases crossed (Clover, Dahlia) the only other Checkout-session break that
|
|
76
|
+
* reaches a param built here is Clover's switch of new subscriptions to
|
|
77
|
+
* FLEXIBLE billing mode — so a subscription session pins
|
|
78
|
+
* `billing_mode[type]=classic` below, and the subscription it creates bills
|
|
79
|
+
* exactly as the hosted path's does. The measurement below was taken at
|
|
80
|
+
* basil; re-take it in test mode before `release_native_checkout` is turned
|
|
81
|
+
* on anywhere.
|
|
82
|
+
*
|
|
68
83
|
* Same probe, same params (line items, a manual tax line,
|
|
69
84
|
* `shipping_address_collection`, a `fixed_amount` shipping option, Connect
|
|
70
85
|
* `transfer_data[destination]` and `application_fee_amount`), hosted vs custom:
|
|
@@ -81,9 +96,9 @@
|
|
|
81
96
|
* the redirect. An in-page flow makes trusting the browser tempting; the
|
|
82
97
|
* browser is the one participant that can lie about having paid.
|
|
83
98
|
*/ /**
|
|
84
|
-
* The API version `ui_mode: '
|
|
99
|
+
* The API version `ui_mode: 'elements'` needs, pinned on the session-create
|
|
85
100
|
* request only. Stated once so the header and the doc comment cannot drift.
|
|
86
|
-
*/ export const NATIVE_CHECKOUT_STRIPE_VERSION = '
|
|
101
|
+
*/ export const NATIVE_CHECKOUT_STRIPE_VERSION = '2026-03-25.dahlia';
|
|
87
102
|
/** The flag both halves of native checkout share (AGL-1132, AGL-1944). */ export const NATIVE_CHECKOUT_FLAG = 'release_native_checkout';
|
|
88
103
|
/**
|
|
89
104
|
* Two conditions, not one — the same pairing the console's route settled on.
|
|
@@ -149,15 +164,21 @@
|
|
|
149
164
|
* changing what the shopper is charged.
|
|
150
165
|
*
|
|
151
166
|
* `returnUrl` is the caller's OWN `success_url` string, `{CHECKOUT_SESSION_ID}`
|
|
152
|
-
* placeholder and all. Stripe substitutes it on
|
|
167
|
+
* placeholder and all. Stripe substitutes it on an embedded-components return
|
|
153
168
|
* exactly as it does on a hosted redirect (measured), so the storefront's
|
|
154
169
|
* post-purchase reporting — which reads `session_id` off the query to name the
|
|
155
170
|
* order it just completed (AGL-1641) — keeps working unchanged.
|
|
156
171
|
*/ export function applyNativeCheckoutParams(params, returnUrl) {
|
|
157
172
|
params.delete('success_url');
|
|
158
173
|
params.delete('cancel_url');
|
|
159
|
-
params.set('ui_mode', '
|
|
174
|
+
params.set('ui_mode', 'elements');
|
|
160
175
|
params.set('return_url', returnUrl);
|
|
176
|
+
// The pinned version defaults a NEW subscription to flexible billing mode
|
|
177
|
+
// (Clover); the hosted path, on the account default, creates classic ones.
|
|
178
|
+
// Stated so both paths bill a subscription the same way.
|
|
179
|
+
if (params.get('mode') === 'subscription') {
|
|
180
|
+
params.set('subscription_data[billing_mode][type]', 'classic');
|
|
181
|
+
}
|
|
161
182
|
}
|
|
162
183
|
/**
|
|
163
184
|
* The `Stripe-Version` pin, as headers to spread into the session-create fetch.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/native-checkout.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { isServerReleaseFlagOnForOrg } from '@aglyn/tenant-data-admin'\n\n/**\n * The storefront Payment Element (AGL-1944), the half of native payments\n * AGL-1132 said mattered more: a shopper on a merchant's own domain pays\n * without being bounced to `checkout.stripe.com` and back.\n *\n * ## Why a Checkout Session and not a bare PaymentIntent\n *\n * AGL-1132 recommended \"PaymentIntent + `<PaymentElement/>`\" for this surface,\n * and AGL-1944 asked for that to be a conscious re-decision rather than an\n * inherited one. It is: the Payment Element ships, the bare PaymentIntent does\n * not, and the difference is worth stating because it is the whole safety\n * argument.\n *\n * A PaymentIntent-backed flow would have to own totals, tax, shipping and the\n * platform fee itself — which means a SECOND place that computes what a shopper\n * owes. Two paths that price a cart independently is how a storefront starts\n * under-collecting tax without anyone noticing: nothing fails, the number is\n * just quietly wrong on one of them. It also emits `payment_intent.succeeded`\n * rather than `checkout.session.completed`, so every fulfilment branch in\n * `billing-webhook.ts` — orders, inventory, coupons, gift cards, licence keys,\n * subscriptions — would need a parallel implementation reading a different\n * object shape.\n *\n * `ui_mode: 'custom'` is the Payment Element backed by a Checkout Session. The\n * shopper sees `<PaymentElement/>` inside the merchant's own layout — no Stripe\n * chrome, which is the thing AGL-1944 objected to about embedded Checkout — and\n * the server keeps the session builder it already has. So:\n *\n * - tax, shipping, inventory, coupons, the Connect destination and the fee\n * ladder are the SAME code, not a parallel implementation. Parity is by\n * construction rather than by test;\n * - `checkout.session.completed` still fires, so `billing-webhook.ts` needs\n * no change at all and stays the only thing that fulfils an order.\n *\n * ## The API version, and why it is pinned per-request\n *\n * Measured against live Stripe in test mode: `ui_mode: 'custom'` is refused at\n * the platform account's default API version with\n *\n * Invalid Stripe API version: 2020-08-27. In order to use `ui_mode:\n * custom`, you must upgrade to Stripe API version 2025-03-31.basil.\n *\n * Upgrading the ACCOUNT default would re-version every Stripe call in this\n * repo at once — a change whose blast radius nobody could review in one pass.\n * A `Stripe-Version` header pins one request instead, and the pin is contained\n * in a way worth being explicit about: webhook deliveries are versioned per\n * ENDPOINT, not per request, so the events `billing-webhook.ts` parses keep\n * arriving in exactly the shape it already reads. The only thing read off the\n * pinned response is `client_secret` and `id`, neither of which the version\n * affects.\n *\n * Same probe, same params (line items, a manual tax line,\n * `shipping_address_collection`, a `fixed_amount` shipping option, Connect\n * `transfer_data[destination]` and `application_fee_amount`), hosted vs custom:\n * `amount_total` **6212 in both**. That is the tax-and-shipping parity claim,\n * measured rather than assumed.\n *\n * ## What this module deliberately does NOT do\n *\n * It does not fulfil anything, and it hands the browser nothing it could use to\n * claim an order was paid. The client gets a `clientSecret` — which is an\n * instruction to Stripe, not an assertion about money — and the same\n * `session_id` the redirect flow already put in the return URL. Fulfilment\n * remains `checkout.session.completed` in `billing-webhook.ts`, exactly as with\n * the redirect. An in-page flow makes trusting the browser tempting; the\n * browser is the one participant that can lie about having paid.\n */\n\n/**\n * The API version `ui_mode: 'custom'` needs, pinned on the session-create\n * request only. Stated once so the header and the doc comment cannot drift.\n */\nexport const NATIVE_CHECKOUT_STRIPE_VERSION = '2025-03-31.basil'\n\n/** The flag both halves of native checkout share (AGL-1132, AGL-1944). */\nexport const NATIVE_CHECKOUT_FLAG = 'release_native_checkout' as const\n\nexport interface NativeCheckoutMode {\n /** True only when the flag is on for this org AND a publishable key exists. */\n native: boolean\n /** Empty unless `native` — the client needs it to boot Stripe.js. */\n publishableKey: string\n}\n\n/**\n * Two conditions, not one — the same pairing the console's route settled on.\n *\n * A `ui_mode` session returns a `client_secret` and NO `url`, so a browser that\n * cannot mount Stripe.js has nothing to navigate to. If the flag alone decided\n * this, flipping it in an environment without\n * `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` would replace a working redirect with a\n * dead Buy button on every storefront at once. Requiring the key here means the\n * worst case of a premature flag flip is the redirect we already ship.\n *\n * That is not hypothetical for the storefront. Measured on 2026-08-18 through\n * the Vercel REST API (`vercel env ls` cannot see team-shared vars):\n * `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` exists as a project-level var on\n * `aglyn-console` and is set NOWHERE on `aglyn-tenant`, which is the project\n * that serves storefronts. So today this resolves to `native: false` in\n * production however the flag is set, and the shopper keeps the redirect.\n *\n * The org — never the host — is the rollout bucket, per AGL-1656: a hostId and\n * an orgId hash to different buckets, so bucketing on the host could land a\n * merchant's storefront and their console on opposite sides of the same\n * percentage.\n */\nexport async function resolveNativeCheckoutMode(\n orgId: string | null | undefined,\n): Promise<NativeCheckoutMode> {\n const publishableKey = String(\n process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ?? '',\n ).trim()\n if (!publishableKey) return { native: false, publishableKey: '' }\n // A flag lookup that throws must not take a sale down with it, and the catch\n // is a `try` rather than `.catch()` deliberately: Remote Config being\n // unreachable rejects, but the resolver being absent entirely — an\n // environment where the module never loaded — throws SYNCHRONOUSLY, before\n // there is a promise to attach a handler to. Only the block form covers both.\n // Either way the answer is the same and it is the safe one: the redirect.\n let on: boolean\n try {\n on = await isServerReleaseFlagOnForOrg(NATIVE_CHECKOUT_FLAG, orgId ?? null)\n } catch {\n return { native: false, publishableKey: '' }\n }\n return on ? { native: true, publishableKey } : { native: false, publishableKey: '' }\n}\n\n/**\n * Rewrite a built session param set for the Payment Element, in place.\n *\n * DELETES `success_url` and `cancel_url` rather than leaving them alongside\n * `return_url`: Stripe rejects the pair outright on a `ui_mode` session, and a\n * 400 here would surface to the shopper as \"Checkout failed\" on a store that\n * was working an hour ago. Everything else in the param set — line items, the\n * tax line, `automatic_tax`, shipping options, the Connect destination, the\n * application fee, every `metadata[…]` key the webhook reads — is left exactly\n * as the caller built it. That is the point: this function must be incapable of\n * changing what the shopper is charged.\n *\n * `returnUrl` is the caller's OWN `success_url` string, `{CHECKOUT_SESSION_ID}`\n * placeholder and all. Stripe substitutes it on a `ui_mode: 'custom'` return\n * exactly as it does on a hosted redirect (measured), so the storefront's\n * post-purchase reporting — which reads `session_id` off the query to name the\n * order it just completed (AGL-1641) — keeps working unchanged.\n */\nexport function applyNativeCheckoutParams(\n params: URLSearchParams,\n returnUrl: string,\n): void {\n params.delete('success_url')\n params.delete('cancel_url')\n params.set('ui_mode', 'custom')\n params.set('return_url', returnUrl)\n}\n\n/**\n * The `Stripe-Version` pin, as headers to spread into the session-create fetch.\n * Empty for the hosted path so its request is byte-identical to today's.\n */\nexport function nativeCheckoutStripeHeaders(\n mode: NativeCheckoutMode,\n): Record<string, string> {\n return mode.native\n ? { 'Stripe-Version': NATIVE_CHECKOUT_STRIPE_VERSION }\n : {}\n}\n\nexport interface NativeCheckoutPayload {\n /** Stripe's own instruction to the browser. Not a claim that money moved. */\n clientSecret: string\n publishableKey: string\n /** The order document id, and the `session_id` the return URL carries. */\n sessionId: string\n}\n\nexport interface HostedCheckoutPayload {\n url: string\n}\n\n/**\n * Read the created session into the payload the storefront gets back.\n *\n * Returns `null` when the session is unusable, which BOTH callers already treat\n * as \"nothing was sold\" — release the idempotency claim and answer 502. The\n * null case is the whole reason this is a function rather than a ternary: the\n * hosted path's liveness check was `!session.url`, and a `ui_mode` session\n * never has one. Left unchanged, switching a store to native mode would have\n * made every successful session look like a Stripe failure — a 502 to the\n * shopper, a released claim, and a real Checkout Session left open on the\n * account. So the check has to move with the mode, and it does.\n */\nexport function readCheckoutSessionPayload(\n session: { url?: string; id?: string; client_secret?: string },\n mode: NativeCheckoutMode,\n): NativeCheckoutPayload | HostedCheckoutPayload | null {\n if (!mode.native) return session?.url ? { url: session.url } : null\n if (!session?.client_secret || !session?.id) return null\n return {\n clientSecret: String(session.client_secret),\n publishableKey: mode.publishableKey,\n sessionId: String(session.id),\n }\n}\n"],"names":["isServerReleaseFlagOnForOrg","NATIVE_CHECKOUT_STRIPE_VERSION","NATIVE_CHECKOUT_FLAG","resolveNativeCheckoutMode","orgId","process","publishableKey","String","env","NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY","trim","native","on","applyNativeCheckoutParams","params","returnUrl","delete","set","nativeCheckoutStripeHeaders","mode","readCheckoutSessionPayload","session","url","client_secret","id","clientSecret","sessionId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,2BAA2B,QAAQ,2BAA0B;AAEtE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkEC,GAED;;;CAGC,GACD,OAAO,MAAMC,iCAAiC,mBAAkB;AAEhE,wEAAwE,GACxE,OAAO,MAAMC,uBAAuB,0BAAkC;AAStE;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,eAAeC,0BACpBC,KAAgC;QAG9BC;IADF,MAAMC,iBAAiBC,QACrBF,kDAAAA,QAAQG,GAAG,CAACC,kCAAkC,YAA9CJ,kDAAkD,IAClDK,IAAI;IACN,IAAI,CAACJ,gBAAgB,OAAO;QAAEK,QAAQ;QAAOL,gBAAgB;IAAG;IAChE,6EAA6E;IAC7E,sEAAsE;IACtE,mEAAmE;IACnE,2EAA2E;IAC3E,8EAA8E;IAC9E,0EAA0E;IAC1E,IAAIM;IACJ,IAAI;QACFA,KAAK,MAAMZ,4BAA4BE,sBAAsBE,gBAAAA,QAAS;IACxE,EAAE,eAAM;QACN,OAAO;YAAEO,QAAQ;YAAOL,gBAAgB;QAAG;IAC7C;IACA,OAAOM,KAAK;QAAED,QAAQ;QAAML;IAAe,IAAI;QAAEK,QAAQ;QAAOL,gBAAgB;IAAG;AACrF;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASO,0BACdC,MAAuB,EACvBC,SAAiB;IAEjBD,OAAOE,MAAM,CAAC;IACdF,OAAOE,MAAM,CAAC;IACdF,OAAOG,GAAG,CAAC,WAAW;IACtBH,OAAOG,GAAG,CAAC,cAAcF;AAC3B;AAEA;;;CAGC,GACD,OAAO,SAASG,4BACdC,IAAwB;IAExB,OAAOA,KAAKR,MAAM,GACd;QAAE,kBAAkBV;IAA+B,IACnD,CAAC;AACP;AAcA;;;;;;;;;;;CAWC,GACD,OAAO,SAASmB,2BACdC,OAA8D,EAC9DF,IAAwB;IAExB,IAAI,CAACA,KAAKR,MAAM,EAAE,OAAOU,CAAAA,2BAAAA,QAASC,GAAG,IAAG;QAAEA,KAAKD,QAAQC,GAAG;IAAC,IAAI;IAC/D,IAAI,EAACD,2BAAAA,QAASE,aAAa,KAAI,EAACF,2BAAAA,QAASG,EAAE,GAAE,OAAO;IACpD,OAAO;QACLC,cAAclB,OAAOc,QAAQE,aAAa;QAC1CjB,gBAAgBa,KAAKb,cAAc;QACnCoB,WAAWnB,OAAOc,QAAQG,EAAE;IAC9B;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/native-checkout.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { isServerReleaseFlagOnForOrg } from '@aglyn/tenant-data-admin'\n\n/**\n * The storefront Payment Element (AGL-1944), the half of native payments\n * AGL-1132 said mattered more: a shopper on a merchant's own domain pays\n * without being bounced to `checkout.stripe.com` and back.\n *\n * ## Why a Checkout Session and not a bare PaymentIntent\n *\n * AGL-1132 recommended \"PaymentIntent + `<PaymentElement/>`\" for this surface,\n * and AGL-1944 asked for that to be a conscious re-decision rather than an\n * inherited one. It is: the Payment Element ships, the bare PaymentIntent does\n * not, and the difference is worth stating because it is the whole safety\n * argument.\n *\n * A PaymentIntent-backed flow would have to own totals, tax, shipping and the\n * platform fee itself — which means a SECOND place that computes what a shopper\n * owes. Two paths that price a cart independently is how a storefront starts\n * under-collecting tax without anyone noticing: nothing fails, the number is\n * just quietly wrong on one of them. It also emits `payment_intent.succeeded`\n * rather than `checkout.session.completed`, so every fulfilment branch in\n * `billing-webhook.ts` — orders, inventory, coupons, gift cards, licence keys,\n * subscriptions — would need a parallel implementation reading a different\n * object shape.\n *\n * `ui_mode: 'elements'` is the Payment Element backed by a Checkout Session\n * (`'custom'` before the 2026-03-25.dahlia API renamed it). The\n * shopper sees `<PaymentElement/>` inside the merchant's own layout — no Stripe\n * chrome, which is the thing AGL-1944 objected to about embedded Checkout — and\n * the server keeps the session builder it already has. So:\n *\n * - tax, shipping, inventory, coupons, the Connect destination and the fee\n * ladder are the SAME code, not a parallel implementation. Parity is by\n * construction rather than by test;\n * - `checkout.session.completed` still fires, so `billing-webhook.ts` needs\n * no change at all and stays the only thing that fulfils an order.\n *\n * ## The API version, and why it is pinned per-request\n *\n * Measured against live Stripe in test mode: `ui_mode: 'custom'` was refused at\n * the platform account's default API version with\n *\n * Invalid Stripe API version: 2020-08-27. In order to use `ui_mode:\n * custom`, you must upgrade to Stripe API version 2025-03-31.basil.\n *\n * Upgrading the ACCOUNT default would re-version every Stripe call in this\n * repo at once — a change whose blast radius nobody could review in one pass.\n * A `Stripe-Version` header pins one request instead, and the pin is contained\n * in a way worth being explicit about: webhook deliveries are versioned per\n * ENDPOINT, not per request, so the events `billing-webhook.ts` parses keep\n * arriving in exactly the shape it already reads. The only thing read off the\n * pinned response is `client_secret` and `id`, neither of which the version\n * affects.\n *\n * ### Dahlia (AGL-3410)\n *\n * The pin moved from `2025-03-31.basil` to `2026-03-25.dahlia` with the\n * Stripe.js 9 / react-stripe-js 6 upgrade, because the browser half now boots\n * Stripe.js's Dahlia release train, and Dahlia renamed the mode this module\n * sets: `custom` is refused outright and `elements` replaces it. Across the two\n * releases crossed (Clover, Dahlia) the only other Checkout-session break that\n * reaches a param built here is Clover's switch of new subscriptions to\n * FLEXIBLE billing mode — so a subscription session pins\n * `billing_mode[type]=classic` below, and the subscription it creates bills\n * exactly as the hosted path's does. The measurement below was taken at\n * basil; re-take it in test mode before `release_native_checkout` is turned\n * on anywhere.\n *\n * Same probe, same params (line items, a manual tax line,\n * `shipping_address_collection`, a `fixed_amount` shipping option, Connect\n * `transfer_data[destination]` and `application_fee_amount`), hosted vs custom:\n * `amount_total` **6212 in both**. That is the tax-and-shipping parity claim,\n * measured rather than assumed.\n *\n * ## What this module deliberately does NOT do\n *\n * It does not fulfil anything, and it hands the browser nothing it could use to\n * claim an order was paid. The client gets a `clientSecret` — which is an\n * instruction to Stripe, not an assertion about money — and the same\n * `session_id` the redirect flow already put in the return URL. Fulfilment\n * remains `checkout.session.completed` in `billing-webhook.ts`, exactly as with\n * the redirect. An in-page flow makes trusting the browser tempting; the\n * browser is the one participant that can lie about having paid.\n */\n\n/**\n * The API version `ui_mode: 'elements'` needs, pinned on the session-create\n * request only. Stated once so the header and the doc comment cannot drift.\n */\nexport const NATIVE_CHECKOUT_STRIPE_VERSION = '2026-03-25.dahlia'\n\n/** The flag both halves of native checkout share (AGL-1132, AGL-1944). */\nexport const NATIVE_CHECKOUT_FLAG = 'release_native_checkout' as const\n\nexport interface NativeCheckoutMode {\n /** True only when the flag is on for this org AND a publishable key exists. */\n native: boolean\n /** Empty unless `native` — the client needs it to boot Stripe.js. */\n publishableKey: string\n}\n\n/**\n * Two conditions, not one — the same pairing the console's route settled on.\n *\n * A `ui_mode` session returns a `client_secret` and NO `url`, so a browser that\n * cannot mount Stripe.js has nothing to navigate to. If the flag alone decided\n * this, flipping it in an environment without\n * `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` would replace a working redirect with a\n * dead Buy button on every storefront at once. Requiring the key here means the\n * worst case of a premature flag flip is the redirect we already ship.\n *\n * That is not hypothetical for the storefront. Measured on 2026-08-18 through\n * the Vercel REST API (`vercel env ls` cannot see team-shared vars):\n * `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` exists as a project-level var on\n * `aglyn-console` and is set NOWHERE on `aglyn-tenant`, which is the project\n * that serves storefronts. So today this resolves to `native: false` in\n * production however the flag is set, and the shopper keeps the redirect.\n *\n * The org — never the host — is the rollout bucket, per AGL-1656: a hostId and\n * an orgId hash to different buckets, so bucketing on the host could land a\n * merchant's storefront and their console on opposite sides of the same\n * percentage.\n */\nexport async function resolveNativeCheckoutMode(\n orgId: string | null | undefined,\n): Promise<NativeCheckoutMode> {\n const publishableKey = String(\n process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ?? '',\n ).trim()\n if (!publishableKey) return { native: false, publishableKey: '' }\n // A flag lookup that throws must not take a sale down with it, and the catch\n // is a `try` rather than `.catch()` deliberately: Remote Config being\n // unreachable rejects, but the resolver being absent entirely — an\n // environment where the module never loaded — throws SYNCHRONOUSLY, before\n // there is a promise to attach a handler to. Only the block form covers both.\n // Either way the answer is the same and it is the safe one: the redirect.\n let on: boolean\n try {\n on = await isServerReleaseFlagOnForOrg(NATIVE_CHECKOUT_FLAG, orgId ?? null)\n } catch {\n return { native: false, publishableKey: '' }\n }\n return on ? { native: true, publishableKey } : { native: false, publishableKey: '' }\n}\n\n/**\n * Rewrite a built session param set for the Payment Element, in place.\n *\n * DELETES `success_url` and `cancel_url` rather than leaving them alongside\n * `return_url`: Stripe rejects the pair outright on a `ui_mode` session, and a\n * 400 here would surface to the shopper as \"Checkout failed\" on a store that\n * was working an hour ago. Everything else in the param set — line items, the\n * tax line, `automatic_tax`, shipping options, the Connect destination, the\n * application fee, every `metadata[…]` key the webhook reads — is left exactly\n * as the caller built it. That is the point: this function must be incapable of\n * changing what the shopper is charged.\n *\n * `returnUrl` is the caller's OWN `success_url` string, `{CHECKOUT_SESSION_ID}`\n * placeholder and all. Stripe substitutes it on an embedded-components return\n * exactly as it does on a hosted redirect (measured), so the storefront's\n * post-purchase reporting — which reads `session_id` off the query to name the\n * order it just completed (AGL-1641) — keeps working unchanged.\n */\nexport function applyNativeCheckoutParams(\n params: URLSearchParams,\n returnUrl: string,\n): void {\n params.delete('success_url')\n params.delete('cancel_url')\n params.set('ui_mode', 'elements')\n params.set('return_url', returnUrl)\n // The pinned version defaults a NEW subscription to flexible billing mode\n // (Clover); the hosted path, on the account default, creates classic ones.\n // Stated so both paths bill a subscription the same way.\n if (params.get('mode') === 'subscription') {\n params.set('subscription_data[billing_mode][type]', 'classic')\n }\n}\n\n/**\n * The `Stripe-Version` pin, as headers to spread into the session-create fetch.\n * Empty for the hosted path so its request is byte-identical to today's.\n */\nexport function nativeCheckoutStripeHeaders(\n mode: NativeCheckoutMode,\n): Record<string, string> {\n return mode.native\n ? { 'Stripe-Version': NATIVE_CHECKOUT_STRIPE_VERSION }\n : {}\n}\n\nexport interface NativeCheckoutPayload {\n /** Stripe's own instruction to the browser. Not a claim that money moved. */\n clientSecret: string\n publishableKey: string\n /** The order document id, and the `session_id` the return URL carries. */\n sessionId: string\n}\n\nexport interface HostedCheckoutPayload {\n url: string\n}\n\n/**\n * Read the created session into the payload the storefront gets back.\n *\n * Returns `null` when the session is unusable, which BOTH callers already treat\n * as \"nothing was sold\" — release the idempotency claim and answer 502. The\n * null case is the whole reason this is a function rather than a ternary: the\n * hosted path's liveness check was `!session.url`, and a `ui_mode` session\n * never has one. Left unchanged, switching a store to native mode would have\n * made every successful session look like a Stripe failure — a 502 to the\n * shopper, a released claim, and a real Checkout Session left open on the\n * account. So the check has to move with the mode, and it does.\n */\nexport function readCheckoutSessionPayload(\n session: { url?: string; id?: string; client_secret?: string },\n mode: NativeCheckoutMode,\n): NativeCheckoutPayload | HostedCheckoutPayload | null {\n if (!mode.native) return session?.url ? { url: session.url } : null\n if (!session?.client_secret || !session?.id) return null\n return {\n clientSecret: String(session.client_secret),\n publishableKey: mode.publishableKey,\n sessionId: String(session.id),\n }\n}\n"],"names":["isServerReleaseFlagOnForOrg","NATIVE_CHECKOUT_STRIPE_VERSION","NATIVE_CHECKOUT_FLAG","resolveNativeCheckoutMode","orgId","process","publishableKey","String","env","NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY","trim","native","on","applyNativeCheckoutParams","params","returnUrl","delete","set","get","nativeCheckoutStripeHeaders","mode","readCheckoutSessionPayload","session","url","client_secret","id","clientSecret","sessionId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,2BAA2B,QAAQ,2BAA0B;AAEtE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiFC,GAED;;;CAGC,GACD,OAAO,MAAMC,iCAAiC,oBAAmB;AAEjE,wEAAwE,GACxE,OAAO,MAAMC,uBAAuB,0BAAkC;AAStE;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,eAAeC,0BACpBC,KAAgC;QAG9BC;IADF,MAAMC,iBAAiBC,QACrBF,kDAAAA,QAAQG,GAAG,CAACC,kCAAkC,YAA9CJ,kDAAkD,IAClDK,IAAI;IACN,IAAI,CAACJ,gBAAgB,OAAO;QAAEK,QAAQ;QAAOL,gBAAgB;IAAG;IAChE,6EAA6E;IAC7E,sEAAsE;IACtE,mEAAmE;IACnE,2EAA2E;IAC3E,8EAA8E;IAC9E,0EAA0E;IAC1E,IAAIM;IACJ,IAAI;QACFA,KAAK,MAAMZ,4BAA4BE,sBAAsBE,gBAAAA,QAAS;IACxE,EAAE,eAAM;QACN,OAAO;YAAEO,QAAQ;YAAOL,gBAAgB;QAAG;IAC7C;IACA,OAAOM,KAAK;QAAED,QAAQ;QAAML;IAAe,IAAI;QAAEK,QAAQ;QAAOL,gBAAgB;IAAG;AACrF;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASO,0BACdC,MAAuB,EACvBC,SAAiB;IAEjBD,OAAOE,MAAM,CAAC;IACdF,OAAOE,MAAM,CAAC;IACdF,OAAOG,GAAG,CAAC,WAAW;IACtBH,OAAOG,GAAG,CAAC,cAAcF;IACzB,0EAA0E;IAC1E,2EAA2E;IAC3E,yDAAyD;IACzD,IAAID,OAAOI,GAAG,CAAC,YAAY,gBAAgB;QACzCJ,OAAOG,GAAG,CAAC,yCAAyC;IACtD;AACF;AAEA;;;CAGC,GACD,OAAO,SAASE,4BACdC,IAAwB;IAExB,OAAOA,KAAKT,MAAM,GACd;QAAE,kBAAkBV;IAA+B,IACnD,CAAC;AACP;AAcA;;;;;;;;;;;CAWC,GACD,OAAO,SAASoB,2BACdC,OAA8D,EAC9DF,IAAwB;IAExB,IAAI,CAACA,KAAKT,MAAM,EAAE,OAAOW,CAAAA,2BAAAA,QAASC,GAAG,IAAG;QAAEA,KAAKD,QAAQC,GAAG;IAAC,IAAI;IAC/D,IAAI,EAACD,2BAAAA,QAASE,aAAa,KAAI,EAACF,2BAAAA,QAASG,EAAE,GAAE,OAAO;IACpD,OAAO;QACLC,cAAcnB,OAAOe,QAAQE,aAAa;QAC1ClB,gBAAgBc,KAAKd,cAAc;QACnCqB,WAAWpB,OAAOe,QAAQG,EAAE;IAC9B;AACF"}
|