@aglyn/shared-ui-jsx 1.2.0 → 1.4.0
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 +8 -6
- package/src/lib/components/blocked-control.component.d.ts +6 -3
- package/src/lib/components/blocked-control.component.js +6 -4
- package/src/lib/components/blocked-control.component.js.map +1 -1
- package/src/lib/stripe/billing-address-form.component.d.ts +38 -0
- package/src/lib/stripe/billing-address-form.component.js +118 -0
- package/src/lib/stripe/billing-address-form.component.js.map +1 -0
- package/src/lib/stripe/billing-card-form.component.d.ts +56 -0
- package/src/lib/stripe/billing-card-form.component.js +126 -0
- package/src/lib/stripe/billing-card-form.component.js.map +1 -0
- package/src/lib/stripe/browser-stripe.d.ts +20 -0
- package/src/lib/stripe/browser-stripe.js +39 -0
- package/src/lib/stripe/browser-stripe.js.map +1 -0
- package/src/lib/stripe/lazy-stripe.d.ts +42 -0
- package/src/lib/stripe/lazy-stripe.js +56 -0
- package/src/lib/stripe/lazy-stripe.js.map +1 -0
- package/src/lib/stripe/stripe-elements-appearance.d.ts +76 -0
- package/src/lib/stripe/stripe-elements-appearance.js +117 -0
- package/src/lib/stripe/stripe-elements-appearance.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/shared-ui-jsx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,12 +25,14 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/shared-data-mdi": "1.
|
|
29
|
-
"@aglyn/shared-ui-snackstack": "1.
|
|
30
|
-
"@aglyn/shared-ui-theme": "1.
|
|
31
|
-
"@aglyn/shared-util-tools": "1.
|
|
32
|
-
"@aglyn/shared-util-vendor": "1.
|
|
28
|
+
"@aglyn/shared-data-mdi": "1.4.0",
|
|
29
|
+
"@aglyn/shared-ui-snackstack": "1.4.0",
|
|
30
|
+
"@aglyn/shared-ui-theme": "1.4.0",
|
|
31
|
+
"@aglyn/shared-util-tools": "1.4.0",
|
|
32
|
+
"@aglyn/shared-util-vendor": "1.4.0",
|
|
33
33
|
"@mdi/js": "^7.4.47",
|
|
34
|
+
"@stripe/react-stripe-js": "^6.12.0",
|
|
35
|
+
"@stripe/stripe-js": "^9.17.0",
|
|
34
36
|
"@swc/helpers": "0.5.23",
|
|
35
37
|
"change-case": "^5.4.4",
|
|
36
38
|
"clsx": "^2.1.1",
|
|
@@ -40,9 +40,12 @@ export interface BlockedControlProps {
|
|
|
40
40
|
* appears — which would leave a dead button and no reason at all, the exact
|
|
41
41
|
* failure this exists to stop.
|
|
42
42
|
*
|
|
43
|
-
* And the reason lands on the span as a real `aria-label
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* And the reason lands on the span as a real `aria-label`, not only inside
|
|
44
|
+
* the Tooltip's popper: an explanation that appears only on hover says
|
|
45
|
+
* nothing to a screen reader. On a touch device the Tooltip opens on a long
|
|
46
|
+
* press. Not a `title` as well: MUI's Tooltip refuses one on its child (a
|
|
47
|
+
* console error on every render of a blocked control, and two tips on
|
|
48
|
+
* hover), and the label already carries the reason.
|
|
46
49
|
*
|
|
47
50
|
* Both were got right once and then copied; AGL-2113 is the precedent for
|
|
48
51
|
* what copies do — five quota readouts grew their own phrasing and stopped
|
|
@@ -35,9 +35,12 @@ import { cloneElement, isValidElement } from "react";
|
|
|
35
35
|
* appears — which would leave a dead button and no reason at all, the exact
|
|
36
36
|
* failure this exists to stop.
|
|
37
37
|
*
|
|
38
|
-
* And the reason lands on the span as a real `aria-label
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* And the reason lands on the span as a real `aria-label`, not only inside
|
|
39
|
+
* the Tooltip's popper: an explanation that appears only on hover says
|
|
40
|
+
* nothing to a screen reader. On a touch device the Tooltip opens on a long
|
|
41
|
+
* press. Not a `title` as well: MUI's Tooltip refuses one on its child (a
|
|
42
|
+
* console error on every render of a blocked control, and two tips on
|
|
43
|
+
* hover), and the label already carries the reason.
|
|
41
44
|
*
|
|
42
45
|
* Both were got right once and then copied; AGL-2113 is the precedent for
|
|
43
46
|
* what copies do — five quota readouts grew their own phrasing and stopped
|
|
@@ -51,7 +54,6 @@ import { cloneElement, isValidElement } from "react";
|
|
|
51
54
|
children: /*#__PURE__*/ _jsx(Box, {
|
|
52
55
|
component: "span",
|
|
53
56
|
"aria-label": reason,
|
|
54
|
-
title: reason,
|
|
55
57
|
sx: fullWidth ? {
|
|
56
58
|
display: 'flex',
|
|
57
59
|
width: '100%',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/blocked-control.component.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'use client'\n\nimport { Box, Tooltip } from '@mui/material'\nimport { cloneElement, isValidElement, type ReactElement } from 'react'\n\nexport interface BlockedControlProps {\n /**\n * Whether the viewer's standing is RESOLVED and refused. Never true while\n * a claim is still loading: rendering the refusal in that window flashes a\n * disabled button at everybody, including the people who may use it.\n */\n blocked: boolean\n /** Why, in one sentence. Shown on hover, and to a screen reader. */\n reason: string\n /**\n * A single control that accepts a `disabled` prop — MUI's Button,\n * IconButton, Switch, TextField and friends all do.\n */\n children: ReactElement<{ disabled?: boolean }>\n /**\n * Let the wrapper take the full width, for a control that is `fullWidth`\n * or stretched by its stack. The wrapping span is otherwise as wide as its\n * content, which shrinks such a control the moment it is blocked.\n */\n fullWidth?: boolean\n}\n\n/**\n * Renders a control disabled with the reason attached, or passes it through\n * untouched.\n *\n * DISABLED WITH THE REASON, NOT HIDDEN. Both are defensible and the choice is\n * made here once rather than at every gate. Hiding leaves a reader unable to\n * see that the capability exists at all, and the commonest support act is\n * routing — \"this needs someone with the super role\" is an answer they can\n * give in one message; \"I see no such button\" turns into an investigation of\n * whether the feature shipped.\n *\n * ## Two details that are the whole reason this is shared\n *\n * The tooltip wraps a `span`, not the control: MUI does not fire pointer\n * events on a disabled button, so a tooltip attached directly to one never\n * appears — which would leave a dead button and no reason at all, the exact\n * failure this exists to stop.\n *\n * And the reason lands on the span as a real `aria-label
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/blocked-control.component.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'use client'\n\nimport { Box, Tooltip } from '@mui/material'\nimport { cloneElement, isValidElement, type ReactElement } from 'react'\n\nexport interface BlockedControlProps {\n /**\n * Whether the viewer's standing is RESOLVED and refused. Never true while\n * a claim is still loading: rendering the refusal in that window flashes a\n * disabled button at everybody, including the people who may use it.\n */\n blocked: boolean\n /** Why, in one sentence. Shown on hover, and to a screen reader. */\n reason: string\n /**\n * A single control that accepts a `disabled` prop — MUI's Button,\n * IconButton, Switch, TextField and friends all do.\n */\n children: ReactElement<{ disabled?: boolean }>\n /**\n * Let the wrapper take the full width, for a control that is `fullWidth`\n * or stretched by its stack. The wrapping span is otherwise as wide as its\n * content, which shrinks such a control the moment it is blocked.\n */\n fullWidth?: boolean\n}\n\n/**\n * Renders a control disabled with the reason attached, or passes it through\n * untouched.\n *\n * DISABLED WITH THE REASON, NOT HIDDEN. Both are defensible and the choice is\n * made here once rather than at every gate. Hiding leaves a reader unable to\n * see that the capability exists at all, and the commonest support act is\n * routing — \"this needs someone with the super role\" is an answer they can\n * give in one message; \"I see no such button\" turns into an investigation of\n * whether the feature shipped.\n *\n * ## Two details that are the whole reason this is shared\n *\n * The tooltip wraps a `span`, not the control: MUI does not fire pointer\n * events on a disabled button, so a tooltip attached directly to one never\n * appears — which would leave a dead button and no reason at all, the exact\n * failure this exists to stop.\n *\n * And the reason lands on the span as a real `aria-label`, not only inside\n * the Tooltip's popper: an explanation that appears only on hover says\n * nothing to a screen reader. On a touch device the Tooltip opens on a long\n * press. Not a `title` as well: MUI's Tooltip refuses one on its child (a\n * console error on every render of a blocked control, and two tips on\n * hover), and the label already carries the reason.\n *\n * Both were got right once and then copied; AGL-2113 is the precedent for\n * what copies do — five quota readouts grew their own phrasing and stopped\n * agreeing. THIS IS NOT A SECURITY BOUNDARY: the server refuses regardless\n * of what rendered. It exists so a console stops promising what the server\n * will refuse.\n */\nexport function BlockedControl({ blocked, reason, children, fullWidth }: BlockedControlProps) {\n if (!blocked || !isValidElement(children)) return children\n return (\n <Tooltip title={reason}>\n <Box\n component=\"span\"\n aria-label={reason}\n sx={fullWidth ? { display: 'flex', width: '100%', '& > *': { flex: 1 } } : { display: 'inline-flex' }}\n >\n {cloneElement(children, { disabled: true })}\n </Box>\n </Tooltip>\n )\n}\n\nBlockedControl.displayName = 'BlockedControl'\nBlockedControl.aglyn = true\n\nexport default BlockedControl\n"],"names":["Box","Tooltip","cloneElement","isValidElement","BlockedControl","blocked","reason","children","fullWidth","title","component","aria-label","sx","display","width","flex","disabled","displayName","aglyn"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,GAAG,EAAEC,OAAO,QAAQ,gBAAe;AAC5C,SAASC,YAAY,EAAEC,cAAc,QAA2B,QAAO;AAwBvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,SAASC,eAAe,EAAEC,OAAO,EAAEC,MAAM,EAAEC,QAAQ,EAAEC,SAAS,EAAuB;IAC1F,IAAI,CAACH,WAAW,eAACF,eAAeI,WAAW,OAAOA;IAClD,qBACE,KAACN;QAAQQ,OAAOH;kBACd,cAAA,KAACN;YACCU,WAAU;YACVC,cAAYL;YACZM,IAAIJ,YAAY;gBAAEK,SAAS;gBAAQC,OAAO;gBAAQ,SAAS;oBAAEC,MAAM;gBAAE;YAAE,IAAI;gBAAEF,SAAS;YAAc;sBAEnGX,cAAAA,aAAaK,UAAU;gBAAES,UAAU;YAAK;;;AAIjD;AAEAZ,eAAea,WAAW,GAAG;AAC7Bb,eAAec,KAAK,GAAG;AAEvB,eAAed,eAAc"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/** A billing address, in the words the billing profile takes it. */
|
|
18
|
+
export interface BillingAddressValue {
|
|
19
|
+
name: string;
|
|
20
|
+
line1: string;
|
|
21
|
+
line2: string;
|
|
22
|
+
city: string;
|
|
23
|
+
state: string;
|
|
24
|
+
postalCode: string;
|
|
25
|
+
country: string;
|
|
26
|
+
}
|
|
27
|
+
export interface BillingAddressFormProps {
|
|
28
|
+
/** Hand back the address; the server saves it and says whether it took. */
|
|
29
|
+
onConfirmed: (address: BillingAddressValue) => Promise<void> | void;
|
|
30
|
+
onCancel: () => void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A billing address in Stripe's own Address Element, in our theme, for a
|
|
34
|
+
* purchase that needs one before it can be taxed. It holds no card data, and
|
|
35
|
+
* the address goes to the same `set-billing-address` action Billing's own card
|
|
36
|
+
* uses, so the two cannot disagree.
|
|
37
|
+
*/
|
|
38
|
+
export default function BillingAddressFormComponent(props: BillingAddressFormProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
19
|
+
import { AddressElement, Elements, useElements } from "@stripe/react-stripe-js";
|
|
20
|
+
import { Alert, Box, Button, Stack, useColorScheme, useTheme } from "@mui/material";
|
|
21
|
+
import { useMemo, useState } from "react";
|
|
22
|
+
import { getBrowserStripe } from "./browser-stripe.js";
|
|
23
|
+
import { stripeAppearanceFromTheme } from "./stripe-elements-appearance.js";
|
|
24
|
+
function AddressFields({ onConfirmed, onCancel }) {
|
|
25
|
+
const elements = useElements();
|
|
26
|
+
const [busy, setBusy] = useState(false);
|
|
27
|
+
const [error, setError] = useState(null);
|
|
28
|
+
const submit = async ()=>{
|
|
29
|
+
const element = elements == null ? void 0 : elements.getElement('address');
|
|
30
|
+
if (!element) return;
|
|
31
|
+
setBusy(true);
|
|
32
|
+
setError(null);
|
|
33
|
+
try {
|
|
34
|
+
var _value_address_line2;
|
|
35
|
+
const { complete, value } = await element.getValue();
|
|
36
|
+
if (!complete) {
|
|
37
|
+
setError('Enter the whole address: sales tax is calculated from it.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
await onConfirmed({
|
|
41
|
+
name: value.name,
|
|
42
|
+
line1: value.address.line1,
|
|
43
|
+
line2: (_value_address_line2 = value.address.line2) != null ? _value_address_line2 : '',
|
|
44
|
+
city: value.address.city,
|
|
45
|
+
state: value.address.state,
|
|
46
|
+
postalCode: value.address.postal_code,
|
|
47
|
+
country: value.address.country
|
|
48
|
+
});
|
|
49
|
+
} catch (unused) {
|
|
50
|
+
setError('That address could not be saved. Nothing has changed.');
|
|
51
|
+
} finally{
|
|
52
|
+
setBusy(false);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
56
|
+
spacing: 2,
|
|
57
|
+
children: [
|
|
58
|
+
/*#__PURE__*/ _jsx(AddressElement, {
|
|
59
|
+
options: {
|
|
60
|
+
mode: 'billing'
|
|
61
|
+
}
|
|
62
|
+
}),
|
|
63
|
+
error ? /*#__PURE__*/ _jsx(Alert, {
|
|
64
|
+
severity: "warning",
|
|
65
|
+
onClose: ()=>setError(null),
|
|
66
|
+
children: error
|
|
67
|
+
}) : null,
|
|
68
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
69
|
+
direction: "row",
|
|
70
|
+
spacing: 1,
|
|
71
|
+
children: [
|
|
72
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
73
|
+
variant: "contained",
|
|
74
|
+
size: "small",
|
|
75
|
+
disabled: !elements || busy,
|
|
76
|
+
onClick: submit,
|
|
77
|
+
children: busy ? 'Saving…' : 'Save address'
|
|
78
|
+
}),
|
|
79
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
80
|
+
size: "small",
|
|
81
|
+
disabled: busy,
|
|
82
|
+
onClick: onCancel,
|
|
83
|
+
children: 'Cancel'
|
|
84
|
+
})
|
|
85
|
+
]
|
|
86
|
+
})
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* A billing address in Stripe's own Address Element, in our theme, for a
|
|
92
|
+
* purchase that needs one before it can be taxed. It holds no card data, and
|
|
93
|
+
* the address goes to the same `set-billing-address` action Billing's own card
|
|
94
|
+
* uses, so the two cannot disagree.
|
|
95
|
+
*/ export default function BillingAddressFormComponent(props) {
|
|
96
|
+
const theme = useTheme();
|
|
97
|
+
const stripe = useMemo(()=>getBrowserStripe(), []);
|
|
98
|
+
const { mode, systemMode } = useColorScheme();
|
|
99
|
+
// The scheme is not on `theme`, which stays the same object across a flip.
|
|
100
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
|
+
const appearance = useMemo(()=>stripeAppearanceFromTheme(theme), [
|
|
102
|
+
theme,
|
|
103
|
+
mode,
|
|
104
|
+
systemMode
|
|
105
|
+
]);
|
|
106
|
+
if (!stripe) return null;
|
|
107
|
+
return /*#__PURE__*/ _jsx(Box, {
|
|
108
|
+
children: /*#__PURE__*/ _jsx(Elements, {
|
|
109
|
+
stripe: stripe,
|
|
110
|
+
options: {
|
|
111
|
+
appearance
|
|
112
|
+
},
|
|
113
|
+
children: /*#__PURE__*/ _jsx(AddressFields, _extends({}, props))
|
|
114
|
+
})
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//# sourceMappingURL=billing-address-form.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/stripe/billing-address-form.component.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 { AddressElement, Elements, useElements } from '@stripe/react-stripe-js'\nimport { Alert, Box, Button, Stack, useColorScheme, useTheme } from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport { getBrowserStripe } from './browser-stripe'\nimport { stripeAppearanceFromTheme } from './stripe-elements-appearance'\n\n/** A billing address, in the words the billing profile takes it. */\nexport interface BillingAddressValue {\n name: string\n line1: string\n line2: string\n city: string\n state: string\n postalCode: string\n country: string\n}\n\nexport interface BillingAddressFormProps {\n /** Hand back the address; the server saves it and says whether it took. */\n onConfirmed: (address: BillingAddressValue) => Promise<void> | void\n onCancel: () => void\n}\n\nfunction AddressFields({ onConfirmed, onCancel }: BillingAddressFormProps) {\n const elements = useElements()\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const submit = async () => {\n const element = elements?.getElement('address')\n if (!element) return\n setBusy(true)\n setError(null)\n try {\n const { complete, value } = await element.getValue()\n if (!complete) {\n setError('Enter the whole address: sales tax is calculated from it.')\n return\n }\n await onConfirmed({\n name: value.name,\n line1: value.address.line1,\n line2: value.address.line2 ?? '',\n city: value.address.city,\n state: value.address.state,\n postalCode: value.address.postal_code,\n country: value.address.country,\n })\n } catch {\n setError('That address could not be saved. Nothing has changed.')\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <Stack spacing={2}>\n <AddressElement options={{ mode: 'billing' }} />\n {error ? (\n <Alert severity=\"warning\" onClose={() => setError(null)}>\n {error}\n </Alert>\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n <Button variant=\"contained\" size=\"small\" disabled={!elements || busy} onClick={submit}>\n {busy ? 'Saving…' : 'Save address'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={onCancel}>\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n )\n}\n\n/**\n * A billing address in Stripe's own Address Element, in our theme, for a\n * purchase that needs one before it can be taxed. It holds no card data, and\n * the address goes to the same `set-billing-address` action Billing's own card\n * uses, so the two cannot disagree.\n */\nexport default function BillingAddressFormComponent(props: BillingAddressFormProps) {\n const theme = useTheme()\n const stripe = useMemo(() => getBrowserStripe(), [])\n const { mode, systemMode } = useColorScheme()\n // The scheme is not on `theme`, which stays the same object across a flip.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const appearance = useMemo(() => stripeAppearanceFromTheme(theme), [theme, mode, systemMode])\n if (!stripe) return null\n return (\n <Box>\n <Elements stripe={stripe} options={{ appearance }}>\n <AddressFields {...props} />\n </Elements>\n </Box>\n )\n}\n"],"names":["AddressElement","Elements","useElements","Alert","Box","Button","Stack","useColorScheme","useTheme","useMemo","useState","getBrowserStripe","stripeAppearanceFromTheme","AddressFields","onConfirmed","onCancel","elements","busy","setBusy","error","setError","submit","element","getElement","value","complete","getValue","name","line1","address","line2","city","state","postalCode","postal_code","country","spacing","options","mode","severity","onClose","direction","variant","size","disabled","onClick","BillingAddressFormComponent","props","theme","stripe","systemMode","appearance"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;AAEA,SAASA,cAAc,EAAEC,QAAQ,EAAEC,WAAW,QAAQ,0BAAyB;AAC/E,SAASC,KAAK,EAAEC,GAAG,EAAEC,MAAM,EAAEC,KAAK,EAAEC,cAAc,EAAEC,QAAQ,QAAQ,gBAAe;AACnF,SAASC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACzC,SAASC,gBAAgB,QAAQ,sBAAkB;AACnD,SAASC,yBAAyB,QAAQ,kCAA8B;AAmBxE,SAASC,cAAc,EAAEC,WAAW,EAAEC,QAAQ,EAA2B;IACvE,MAAMC,WAAWd;IACjB,MAAM,CAACe,MAAMC,QAAQ,GAAGR,SAAS;IACjC,MAAM,CAACS,OAAOC,SAAS,GAAGV,SAAwB;IAElD,MAAMW,SAAS;QACb,MAAMC,UAAUN,4BAAAA,SAAUO,UAAU,CAAC;QACrC,IAAI,CAACD,SAAS;QACdJ,QAAQ;QACRE,SAAS;QACT,IAAI;gBASOI;YART,MAAM,EAAEC,QAAQ,EAAED,KAAK,EAAE,GAAG,MAAMF,QAAQI,QAAQ;YAClD,IAAI,CAACD,UAAU;gBACbL,SAAS;gBACT;YACF;YACA,MAAMN,YAAY;gBAChBa,MAAMH,MAAMG,IAAI;gBAChBC,OAAOJ,MAAMK,OAAO,CAACD,KAAK;gBAC1BE,KAAK,GAAEN,uBAAAA,MAAMK,OAAO,CAACC,KAAK,YAAnBN,uBAAuB;gBAC9BO,MAAMP,MAAMK,OAAO,CAACE,IAAI;gBACxBC,OAAOR,MAAMK,OAAO,CAACG,KAAK;gBAC1BC,YAAYT,MAAMK,OAAO,CAACK,WAAW;gBACrCC,SAASX,MAAMK,OAAO,CAACM,OAAO;YAChC;QACF,EAAE,eAAM;YACNf,SAAS;QACX,SAAU;YACRF,QAAQ;QACV;IACF;IAEA,qBACE,MAACZ;QAAM8B,SAAS;;0BACd,KAACpC;gBAAeqC,SAAS;oBAAEC,MAAM;gBAAU;;YAC1CnB,sBACC,KAAChB;gBAAMoC,UAAS;gBAAUC,SAAS,IAAMpB,SAAS;0BAC/CD;iBAED;0BACJ,MAACb;gBAAMmC,WAAU;gBAAML,SAAS;;kCAC9B,KAAC/B;wBAAOqC,SAAQ;wBAAYC,MAAK;wBAAQC,UAAU,CAAC5B,YAAYC;wBAAM4B,SAASxB;kCAC5EJ,OAAO,YAAY;;kCAEtB,KAACZ;wBAAOsC,MAAK;wBAAQC,UAAU3B;wBAAM4B,SAAS9B;kCAC3C;;;;;;AAKX;AAEA;;;;;CAKC,GACD,eAAe,SAAS+B,4BAA4BC,KAA8B;IAChF,MAAMC,QAAQxC;IACd,MAAMyC,SAASxC,QAAQ,IAAME,oBAAoB,EAAE;IACnD,MAAM,EAAE2B,IAAI,EAAEY,UAAU,EAAE,GAAG3C;IAC7B,2EAA2E;IAC3E,uDAAuD;IACvD,MAAM4C,aAAa1C,QAAQ,IAAMG,0BAA0BoC,QAAQ;QAACA;QAAOV;QAAMY;KAAW;IAC5F,IAAI,CAACD,QAAQ,OAAO;IACpB,qBACE,KAAC7C;kBACC,cAAA,KAACH;YAASgD,QAAQA;YAAQZ,SAAS;gBAAEc;YAAW;sBAC9C,cAAA,KAACtC,4BAAkBkC;;;AAI3B"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Add a card WITHOUT leaving the page or the design (AGL follow-up to the
|
|
19
|
+
* native billing surface).
|
|
20
|
+
*
|
|
21
|
+
* ## Why this is not a dialog any more
|
|
22
|
+
*
|
|
23
|
+
* It used to open Stripe's hosted checkout in a modal. Being asked for
|
|
24
|
+
* payment details by an interface that has just appeared and looks like a
|
|
25
|
+
* different product is a reason to stop being a customer — and that reaction
|
|
26
|
+
* is about PRESENTATION, not about where the card data goes.
|
|
27
|
+
*
|
|
28
|
+
* Elements answers exactly that half. Each field is its own cross-origin
|
|
29
|
+
* iframe on Stripe's domain, so the guarantee is unchanged — the PAN, the CVC
|
|
30
|
+
* and the expiry are typed into Stripe's document and posted to Stripe, and
|
|
31
|
+
* never touch our DOM, our server or our logs. What changes is that the
|
|
32
|
+
* fields render inline, at our sizes, in our type, with our Save button
|
|
33
|
+
* beside them.
|
|
34
|
+
*
|
|
35
|
+
* ## The part that is easy to get wrong
|
|
36
|
+
*
|
|
37
|
+
* `appearance` must carry LITERAL colors. Elements paints inside Stripe's
|
|
38
|
+
* origin, where our CSS custom properties do not exist, so a
|
|
39
|
+
* `var(--mui-palette-…)` string resolves to nothing and the field silently
|
|
40
|
+
* falls back to Stripe's default styling. `stripeAppearanceFromTheme`
|
|
41
|
+
* resolves them against the document before they cross that boundary.
|
|
42
|
+
*
|
|
43
|
+
* ## Fulfilment still is not here
|
|
44
|
+
*
|
|
45
|
+
* Confirming a SetupIntent saves a card; it grants nothing. The default is
|
|
46
|
+
* set server-side in `finalize-card-setup`, which re-reads the intent from
|
|
47
|
+
* Stripe rather than trusting this component's word that it succeeded.
|
|
48
|
+
*/
|
|
49
|
+
export interface BillingCardFormProps {
|
|
50
|
+
/** SetupIntent client secret from `create-setup-intent`. */
|
|
51
|
+
clientSecret: string;
|
|
52
|
+
/** Hand back the confirmed intent id; the server verifies it from there. */
|
|
53
|
+
onConfirmed: (setupIntentId: string) => Promise<void> | void;
|
|
54
|
+
onCancel: () => void;
|
|
55
|
+
}
|
|
56
|
+
export default function BillingCardFormComponent({ clientSecret, onConfirmed, onCancel, }: BillingCardFormProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
+
import { Elements, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js";
|
|
19
|
+
import { Alert, Box, Button, Stack, useColorScheme, useTheme } from "@mui/material";
|
|
20
|
+
import { useMemo, useState } from "react";
|
|
21
|
+
import { getBrowserStripe } from "./browser-stripe.js";
|
|
22
|
+
import { stripeAppearanceFromTheme } from "./stripe-elements-appearance.js";
|
|
23
|
+
function CardFields({ onConfirmed, onCancel }) {
|
|
24
|
+
const stripe = useStripe();
|
|
25
|
+
const elements = useElements();
|
|
26
|
+
const [busy, setBusy] = useState(false);
|
|
27
|
+
/** Stripe's own refusal for the card just submitted, or null. */ const [error, setError] = useState(null);
|
|
28
|
+
const submit = async ()=>{
|
|
29
|
+
if (!stripe || !elements) return;
|
|
30
|
+
setBusy(true);
|
|
31
|
+
setError(null);
|
|
32
|
+
try {
|
|
33
|
+
var _result_setupIntent;
|
|
34
|
+
// `redirect: 'if_required'` keeps the customer here for an ordinary
|
|
35
|
+
// card. A bank that demands 3DS still gets its own Stripe-hosted step —
|
|
36
|
+
// that one is the issuer's and is not ours to restyle or skip.
|
|
37
|
+
const result = await stripe.confirmSetup({
|
|
38
|
+
elements,
|
|
39
|
+
redirect: 'if_required'
|
|
40
|
+
});
|
|
41
|
+
if (result.error) {
|
|
42
|
+
var _result_error_message;
|
|
43
|
+
// Stripe's own sentence. It names what the issuer or the validator
|
|
44
|
+
// objected to, which is what the customer needs in order to fix it,
|
|
45
|
+
// and it stays true as the rules change.
|
|
46
|
+
setError((_result_error_message = result.error.message) != null ? _result_error_message : 'That card could not be saved.');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const setupIntentId = (_result_setupIntent = result.setupIntent) == null ? void 0 : _result_setupIntent.id;
|
|
50
|
+
if (!setupIntentId) {
|
|
51
|
+
setError('That card could not be saved. Nothing has changed.');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
await onConfirmed(setupIntentId);
|
|
55
|
+
} catch (unused) {
|
|
56
|
+
setError('We could not reach Stripe. Nothing has changed.');
|
|
57
|
+
} finally{
|
|
58
|
+
setBusy(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
62
|
+
spacing: 2,
|
|
63
|
+
children: [
|
|
64
|
+
/*#__PURE__*/ _jsx(PaymentElement, {
|
|
65
|
+
options: {
|
|
66
|
+
layout: 'tabs'
|
|
67
|
+
}
|
|
68
|
+
}),
|
|
69
|
+
error ? /*#__PURE__*/ _jsx(Alert, {
|
|
70
|
+
severity: "warning",
|
|
71
|
+
onClose: ()=>setError(null),
|
|
72
|
+
children: error
|
|
73
|
+
}) : null,
|
|
74
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
75
|
+
direction: "row",
|
|
76
|
+
spacing: 1,
|
|
77
|
+
children: [
|
|
78
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
79
|
+
variant: "contained",
|
|
80
|
+
size: "small",
|
|
81
|
+
disabled: !stripe || busy,
|
|
82
|
+
onClick: submit,
|
|
83
|
+
children: busy ? 'Saving…' : 'Save card'
|
|
84
|
+
}),
|
|
85
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
86
|
+
size: "small",
|
|
87
|
+
disabled: busy,
|
|
88
|
+
onClick: onCancel,
|
|
89
|
+
children: 'Cancel'
|
|
90
|
+
})
|
|
91
|
+
]
|
|
92
|
+
})
|
|
93
|
+
]
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export default function BillingCardFormComponent({ clientSecret, onConfirmed, onCancel }) {
|
|
97
|
+
const theme = useTheme();
|
|
98
|
+
const stripe = useMemo(()=>getBrowserStripe(), []);
|
|
99
|
+
// Rebuilt when the scheme flips, so a customer who switches to dark mode
|
|
100
|
+
// mid-form does not keep a light card form.
|
|
101
|
+
const { mode, systemMode } = useColorScheme();
|
|
102
|
+
const appearance = useMemo(()=>stripeAppearanceFromTheme(theme), // The scheme is not on `theme`, which stays the same object across a flip.
|
|
103
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
104
|
+
[
|
|
105
|
+
theme,
|
|
106
|
+
mode,
|
|
107
|
+
systemMode
|
|
108
|
+
]);
|
|
109
|
+
if (!stripe) return null;
|
|
110
|
+
return /*#__PURE__*/ _jsx(Box, {
|
|
111
|
+
children: /*#__PURE__*/ _jsx(Elements, {
|
|
112
|
+
stripe: stripe,
|
|
113
|
+
options: {
|
|
114
|
+
clientSecret,
|
|
115
|
+
appearance,
|
|
116
|
+
loader: 'always'
|
|
117
|
+
},
|
|
118
|
+
children: /*#__PURE__*/ _jsx(CardFields, {
|
|
119
|
+
onConfirmed: onConfirmed,
|
|
120
|
+
onCancel: onCancel
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=billing-card-form.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/stripe/billing-card-form.component.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 {\n Elements,\n PaymentElement,\n useElements,\n useStripe,\n} from '@stripe/react-stripe-js'\nimport { Alert, Box, Button, Stack, useColorScheme, useTheme } from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport { getBrowserStripe } from './browser-stripe'\nimport { stripeAppearanceFromTheme } from './stripe-elements-appearance'\n\n/**\n * Add a card WITHOUT leaving the page or the design (AGL follow-up to the\n * native billing surface).\n *\n * ## Why this is not a dialog any more\n *\n * It used to open Stripe's hosted checkout in a modal. Being asked for\n * payment details by an interface that has just appeared and looks like a\n * different product is a reason to stop being a customer — and that reaction\n * is about PRESENTATION, not about where the card data goes.\n *\n * Elements answers exactly that half. Each field is its own cross-origin\n * iframe on Stripe's domain, so the guarantee is unchanged — the PAN, the CVC\n * and the expiry are typed into Stripe's document and posted to Stripe, and\n * never touch our DOM, our server or our logs. What changes is that the\n * fields render inline, at our sizes, in our type, with our Save button\n * beside them.\n *\n * ## The part that is easy to get wrong\n *\n * `appearance` must carry LITERAL colors. Elements paints inside Stripe's\n * origin, where our CSS custom properties do not exist, so a\n * `var(--mui-palette-…)` string resolves to nothing and the field silently\n * falls back to Stripe's default styling. `stripeAppearanceFromTheme`\n * resolves them against the document before they cross that boundary.\n *\n * ## Fulfilment still is not here\n *\n * Confirming a SetupIntent saves a card; it grants nothing. The default is\n * set server-side in `finalize-card-setup`, which re-reads the intent from\n * Stripe rather than trusting this component's word that it succeeded.\n */\n\nexport interface BillingCardFormProps {\n /** SetupIntent client secret from `create-setup-intent`. */\n clientSecret: string\n /** Hand back the confirmed intent id; the server verifies it from there. */\n onConfirmed: (setupIntentId: string) => Promise<void> | void\n onCancel: () => void\n}\n\nfunction CardFields({ onConfirmed, onCancel }: Omit<BillingCardFormProps, 'clientSecret'>) {\n const stripe = useStripe()\n const elements = useElements()\n const [busy, setBusy] = useState(false)\n /** Stripe's own refusal for the card just submitted, or null. */\n const [error, setError] = useState<string | null>(null)\n\n const submit = async () => {\n if (!stripe || !elements) return\n setBusy(true)\n setError(null)\n try {\n // `redirect: 'if_required'` keeps the customer here for an ordinary\n // card. A bank that demands 3DS still gets its own Stripe-hosted step —\n // that one is the issuer's and is not ours to restyle or skip.\n const result = await stripe.confirmSetup({\n elements,\n redirect: 'if_required',\n })\n if (result.error) {\n // Stripe's own sentence. It names what the issuer or the validator\n // objected to, which is what the customer needs in order to fix it,\n // and it stays true as the rules change.\n setError(result.error.message ?? 'That card could not be saved.')\n return\n }\n const setupIntentId = result.setupIntent?.id\n if (!setupIntentId) {\n setError('That card could not be saved. Nothing has changed.')\n return\n }\n await onConfirmed(setupIntentId)\n } catch {\n setError('We could not reach Stripe. Nothing has changed.')\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <Stack spacing={2}>\n {/*\n Stripe's fields. There is deliberately no input of ours anywhere in\n this component — `billing-card-entry-stays-in-stripe.spec.ts` sweeps\n for one, and that guard is what keeps \"looks like our form\" from\n drifting into \"is our form\".\n */}\n <PaymentElement options={{ layout: 'tabs' }} />\n {error ? (\n <Alert severity=\"warning\" onClose={() => setError(null)}>\n {error}\n </Alert>\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n size=\"small\"\n disabled={!stripe || busy}\n onClick={submit}\n >\n {busy ? 'Saving…' : 'Save card'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={onCancel}>\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n )\n}\n\nexport default function BillingCardFormComponent({\n clientSecret,\n onConfirmed,\n onCancel,\n}: BillingCardFormProps) {\n const theme = useTheme()\n const stripe = useMemo(() => getBrowserStripe(), [])\n // Rebuilt when the scheme flips, so a customer who switches to dark mode\n // mid-form does not keep a light card form.\n const { mode, systemMode } = useColorScheme()\n const appearance = useMemo(\n () => stripeAppearanceFromTheme(theme),\n // The scheme is not on `theme`, which stays the same object across a flip.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [theme, mode, systemMode],\n )\n if (!stripe) return null\n return (\n <Box>\n {/* Stripe's own skeleton, so a slow first load is not an empty box. */}\n <Elements stripe={stripe} options={{ clientSecret, appearance, loader: 'always' }}>\n <CardFields onConfirmed={onConfirmed} onCancel={onCancel} />\n </Elements>\n </Box>\n )\n}\n"],"names":["Elements","PaymentElement","useElements","useStripe","Alert","Box","Button","Stack","useColorScheme","useTheme","useMemo","useState","getBrowserStripe","stripeAppearanceFromTheme","CardFields","onConfirmed","onCancel","stripe","elements","busy","setBusy","error","setError","submit","result","confirmSetup","redirect","message","setupIntentId","setupIntent","id","spacing","options","layout","severity","onClose","direction","variant","size","disabled","onClick","BillingCardFormComponent","clientSecret","theme","mode","systemMode","appearance","loader"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;AAEA,SACEA,QAAQ,EACRC,cAAc,EACdC,WAAW,EACXC,SAAS,QACJ,0BAAyB;AAChC,SAASC,KAAK,EAAEC,GAAG,EAAEC,MAAM,EAAEC,KAAK,EAAEC,cAAc,EAAEC,QAAQ,QAAQ,gBAAe;AACnF,SAASC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACzC,SAASC,gBAAgB,QAAQ,sBAAkB;AACnD,SAASC,yBAAyB,QAAQ,kCAA8B;AA2CxE,SAASC,WAAW,EAAEC,WAAW,EAAEC,QAAQ,EAA8C;IACvF,MAAMC,SAASd;IACf,MAAMe,WAAWhB;IACjB,MAAM,CAACiB,MAAMC,QAAQ,GAAGT,SAAS;IACjC,+DAA+D,GAC/D,MAAM,CAACU,OAAOC,SAAS,GAAGX,SAAwB;IAElD,MAAMY,SAAS;QACb,IAAI,CAACN,UAAU,CAACC,UAAU;QAC1BE,QAAQ;QACRE,SAAS;QACT,IAAI;gBAeoBE;YAdtB,oEAAoE;YACpE,wEAAwE;YACxE,+DAA+D;YAC/D,MAAMA,SAAS,MAAMP,OAAOQ,YAAY,CAAC;gBACvCP;gBACAQ,UAAU;YACZ;YACA,IAAIF,OAAOH,KAAK,EAAE;oBAIPG;gBAHT,mEAAmE;gBACnE,oEAAoE;gBACpE,yCAAyC;gBACzCF,UAASE,wBAAAA,OAAOH,KAAK,CAACM,OAAO,YAApBH,wBAAwB;gBACjC;YACF;YACA,MAAMI,iBAAgBJ,sBAAAA,OAAOK,WAAW,qBAAlBL,oBAAoBM,EAAE;YAC5C,IAAI,CAACF,eAAe;gBAClBN,SAAS;gBACT;YACF;YACA,MAAMP,YAAYa;QACpB,EAAE,eAAM;YACNN,SAAS;QACX,SAAU;YACRF,QAAQ;QACV;IACF;IAEA,qBACE,MAACb;QAAMwB,SAAS;;0BAOd,KAAC9B;gBAAe+B,SAAS;oBAAEC,QAAQ;gBAAO;;YACzCZ,sBACC,KAACjB;gBAAM8B,UAAS;gBAAUC,SAAS,IAAMb,SAAS;0BAC/CD;iBAED;0BACJ,MAACd;gBAAM6B,WAAU;gBAAML,SAAS;;kCAC9B,KAACzB;wBACC+B,SAAQ;wBACRC,MAAK;wBACLC,UAAU,CAACtB,UAAUE;wBACrBqB,SAASjB;kCAERJ,OAAO,YAAY;;kCAEtB,KAACb;wBAAOgC,MAAK;wBAAQC,UAAUpB;wBAAMqB,SAASxB;kCAC3C;;;;;;AAKX;AAEA,eAAe,SAASyB,yBAAyB,EAC/CC,YAAY,EACZ3B,WAAW,EACXC,QAAQ,EACa;IACrB,MAAM2B,QAAQlC;IACd,MAAMQ,SAASP,QAAQ,IAAME,oBAAoB,EAAE;IACnD,yEAAyE;IACzE,4CAA4C;IAC5C,MAAM,EAAEgC,IAAI,EAAEC,UAAU,EAAE,GAAGrC;IAC7B,MAAMsC,aAAapC,QACjB,IAAMG,0BAA0B8B,QAChC,2EAA2E;IAC3E,uDAAuD;IACvD;QAACA;QAAOC;QAAMC;KAAW;IAE3B,IAAI,CAAC5B,QAAQ,OAAO;IACpB,qBACE,KAACZ;kBAEC,cAAA,KAACL;YAASiB,QAAQA;YAAQe,SAAS;gBAAEU;gBAAcI;gBAAYC,QAAQ;YAAS;sBAC9E,cAAA,KAACjC;gBAAWC,aAAaA;gBAAaC,UAAUA;;;;AAIxD"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type Stripe } from '@stripe/stripe-js';
|
|
18
|
+
export declare function getBrowserStripe(): Promise<Stripe | null> | null;
|
|
19
|
+
/** Whether this browser bundle can render a Stripe payment surface at all. */
|
|
20
|
+
export declare function browserStripeConfigured(): boolean;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { loadStripe } from "@stripe/stripe-js";
|
|
17
|
+
/**
|
|
18
|
+
* ONE browser Stripe instance for the whole console.
|
|
19
|
+
*
|
|
20
|
+
* `loadStripe` injects a script tag, so a per-component singleton means a tag
|
|
21
|
+
* per component that ever mounts. There were two; the SCA step on the billing
|
|
22
|
+
* page would have made a third.
|
|
23
|
+
*
|
|
24
|
+
* Null when `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` is unset, which every caller
|
|
25
|
+
* must handle: it is the state a self-hosted deployment with no Stripe keys is
|
|
26
|
+
* in, and rendering a payment surface against it produces an empty white box
|
|
27
|
+
* that looks like a broken form.
|
|
28
|
+
*/ const publishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
|
|
29
|
+
let stripePromise = null;
|
|
30
|
+
export function getBrowserStripe() {
|
|
31
|
+
if (!publishableKey) return null;
|
|
32
|
+
if (!stripePromise) stripePromise = loadStripe(publishableKey);
|
|
33
|
+
return stripePromise;
|
|
34
|
+
}
|
|
35
|
+
/** Whether this browser bundle can render a Stripe payment surface at all. */ export function browserStripeConfigured() {
|
|
36
|
+
return Boolean(publishableKey);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=browser-stripe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/stripe/browser-stripe.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 { loadStripe, type Stripe } from '@stripe/stripe-js'\n\n/**\n * ONE browser Stripe instance for the whole console.\n *\n * `loadStripe` injects a script tag, so a per-component singleton means a tag\n * per component that ever mounts. There were two; the SCA step on the billing\n * page would have made a third.\n *\n * Null when `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` is unset, which every caller\n * must handle: it is the state a self-hosted deployment with no Stripe keys is\n * in, and rendering a payment surface against it produces an empty white box\n * that looks like a broken form.\n */\nconst publishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY\nlet stripePromise: Promise<Stripe | null> | null = null\n\nexport function getBrowserStripe(): Promise<Stripe | null> | null {\n if (!publishableKey) return null\n if (!stripePromise) stripePromise = loadStripe(publishableKey)\n return stripePromise\n}\n\n/** Whether this browser bundle can render a Stripe payment surface at all. */\nexport function browserStripeConfigured(): boolean {\n return Boolean(publishableKey)\n}\n"],"names":["loadStripe","publishableKey","process","env","NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY","stripePromise","getBrowserStripe","browserStripeConfigured","Boolean"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,QAAqB,oBAAmB;AAE3D;;;;;;;;;;;CAWC,GACD,MAAMC,iBAAiBC,QAAQC,GAAG,CAACC,kCAAkC;AACrE,IAAIC,gBAA+C;AAEnD,OAAO,SAASC;IACd,IAAI,CAACL,gBAAgB,OAAO;IAC5B,IAAI,CAACI,eAAeA,gBAAgBL,WAAWC;IAC/C,OAAOI;AACT;AAEA,4EAA4E,GAC5E,OAAO,SAASE;IACd,OAAOC,QAAQP;AACjB"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* STRIPE, ONLY ONCE SOMEONE IS PAYING (AGL-4049).
|
|
19
|
+
*
|
|
20
|
+
* Importing `@stripe/stripe-js` injects the Stripe script by itself, so a
|
|
21
|
+
* surface that might take a payment must not import it until it does: on a
|
|
22
|
+
* page nobody is buying from, it is the largest thing on the page
|
|
23
|
+
* (`stripe-stays-lazy.spec.ts`).
|
|
24
|
+
*
|
|
25
|
+
* The boundary lives HERE, inside this library, rather than at each caller.
|
|
26
|
+
* A `lazy(() => import('@aglyn/shared-ui-jsx/…'))` in a plugin is a dynamic
|
|
27
|
+
* edge between two projects, and one such edge makes every ordinary import of
|
|
28
|
+
* this library across the console a lint error. This file is the static import
|
|
29
|
+
* they take instead, and it imports nothing of Stripe's.
|
|
30
|
+
*/
|
|
31
|
+
/** The billing card form, loaded when it is first shown. */
|
|
32
|
+
export declare const LazyBillingCardForm: import("react").LazyExoticComponent<typeof import("./billing-card-form.component").default>;
|
|
33
|
+
/** The billing address form, loaded when it is first shown. */
|
|
34
|
+
export declare const LazyBillingAddressForm: import("react").LazyExoticComponent<typeof import("./billing-address-form.component").default>;
|
|
35
|
+
/**
|
|
36
|
+
* Runs the bank's own step for a payment sitting at `requires_action`.
|
|
37
|
+
*
|
|
38
|
+
* Resolves to Stripe's refusal, or `null` once the bank is satisfied. Never
|
|
39
|
+
* throws: a library that will not load is a refusal like any other, and the
|
|
40
|
+
* payment it stopped has not been charged.
|
|
41
|
+
*/
|
|
42
|
+
export declare function handleStripeNextAction(clientSecret: string): Promise<string | null>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { lazy } from "react";
|
|
18
|
+
/**
|
|
19
|
+
* STRIPE, ONLY ONCE SOMEONE IS PAYING (AGL-4049).
|
|
20
|
+
*
|
|
21
|
+
* Importing `@stripe/stripe-js` injects the Stripe script by itself, so a
|
|
22
|
+
* surface that might take a payment must not import it until it does: on a
|
|
23
|
+
* page nobody is buying from, it is the largest thing on the page
|
|
24
|
+
* (`stripe-stays-lazy.spec.ts`).
|
|
25
|
+
*
|
|
26
|
+
* The boundary lives HERE, inside this library, rather than at each caller.
|
|
27
|
+
* A `lazy(() => import('@aglyn/shared-ui-jsx/…'))` in a plugin is a dynamic
|
|
28
|
+
* edge between two projects, and one such edge makes every ordinary import of
|
|
29
|
+
* this library across the console a lint error. This file is the static import
|
|
30
|
+
* they take instead, and it imports nothing of Stripe's.
|
|
31
|
+
*/ /** The billing card form, loaded when it is first shown. */ export const LazyBillingCardForm = /*#__PURE__*/ lazy(()=>import("./billing-card-form.component.js"));
|
|
32
|
+
/** The billing address form, loaded when it is first shown. */ export const LazyBillingAddressForm = /*#__PURE__*/ lazy(()=>import("./billing-address-form.component.js"));
|
|
33
|
+
/**
|
|
34
|
+
* Runs the bank's own step for a payment sitting at `requires_action`.
|
|
35
|
+
*
|
|
36
|
+
* Resolves to Stripe's refusal, or `null` once the bank is satisfied. Never
|
|
37
|
+
* throws: a library that will not load is a refusal like any other, and the
|
|
38
|
+
* payment it stopped has not been charged.
|
|
39
|
+
*/ export async function handleStripeNextAction(clientSecret) {
|
|
40
|
+
try {
|
|
41
|
+
var _result_error_message;
|
|
42
|
+
const { getBrowserStripe } = await import("./browser-stripe.js");
|
|
43
|
+
const stripe = await getBrowserStripe();
|
|
44
|
+
if (!stripe) {
|
|
45
|
+
return 'Your bank needs to confirm this payment, but the payment library could not load.';
|
|
46
|
+
}
|
|
47
|
+
const result = await stripe.handleNextAction({
|
|
48
|
+
clientSecret
|
|
49
|
+
});
|
|
50
|
+
return result.error ? (_result_error_message = result.error.message) != null ? _result_error_message : 'Your bank did not confirm the payment.' : null;
|
|
51
|
+
} catch (unused) {
|
|
52
|
+
return 'Your bank needs to confirm this payment, but the payment library could not load.';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=lazy-stripe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/stripe/lazy-stripe.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 { lazy } from 'react'\n\n/**\n * STRIPE, ONLY ONCE SOMEONE IS PAYING (AGL-4049).\n *\n * Importing `@stripe/stripe-js` injects the Stripe script by itself, so a\n * surface that might take a payment must not import it until it does: on a\n * page nobody is buying from, it is the largest thing on the page\n * (`stripe-stays-lazy.spec.ts`).\n *\n * The boundary lives HERE, inside this library, rather than at each caller.\n * A `lazy(() => import('@aglyn/shared-ui-jsx/…'))` in a plugin is a dynamic\n * edge between two projects, and one such edge makes every ordinary import of\n * this library across the console a lint error. This file is the static import\n * they take instead, and it imports nothing of Stripe's.\n */\n\n/** The billing card form, loaded when it is first shown. */\nexport const LazyBillingCardForm = lazy(() => import('./billing-card-form.component'))\n\n/** The billing address form, loaded when it is first shown. */\nexport const LazyBillingAddressForm = lazy(() => import('./billing-address-form.component'))\n\n/**\n * Runs the bank's own step for a payment sitting at `requires_action`.\n *\n * Resolves to Stripe's refusal, or `null` once the bank is satisfied. Never\n * throws: a library that will not load is a refusal like any other, and the\n * payment it stopped has not been charged.\n */\nexport async function handleStripeNextAction(clientSecret: string): Promise<string | null> {\n try {\n const { getBrowserStripe } = await import('./browser-stripe')\n const stripe = await getBrowserStripe()\n if (!stripe) {\n return 'Your bank needs to confirm this payment, but the payment library could not load.'\n }\n const result = await stripe.handleNextAction({ clientSecret })\n return result.error\n ? (result.error.message ?? 'Your bank did not confirm the payment.')\n : null\n } catch {\n return 'Your bank needs to confirm this payment, but the payment library could not load.'\n }\n}\n"],"names":["lazy","LazyBillingCardForm","LazyBillingAddressForm","handleStripeNextAction","clientSecret","result","getBrowserStripe","stripe","handleNextAction","error","message"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;AAEA,SAASA,IAAI,QAAQ,QAAO;AAE5B;;;;;;;;;;;;;CAaC,GAED,0DAA0D,GAC1D,OAAO,MAAMC,oCAAsBD,KAAK,IAAM,MAAM,CAAC,qCAAiC;AAEtF,6DAA6D,GAC7D,OAAO,MAAME,uCAAyBF,KAAK,IAAM,MAAM,CAAC,wCAAoC;AAE5F;;;;;;CAMC,GACD,OAAO,eAAeG,uBAAuBC,YAAoB;IAC/D,IAAI;YAQGC;QAPL,MAAM,EAAEC,gBAAgB,EAAE,GAAG,MAAM,MAAM,CAAC;QAC1C,MAAMC,SAAS,MAAMD;QACrB,IAAI,CAACC,QAAQ;YACX,OAAO;QACT;QACA,MAAMF,SAAS,MAAME,OAAOC,gBAAgB,CAAC;YAAEJ;QAAa;QAC5D,OAAOC,OAAOI,KAAK,IACdJ,wBAAAA,OAAOI,KAAK,CAACC,OAAO,YAApBL,wBAAwB,2CACzB;IACN,EAAE,eAAM;QACN,OAAO;IACT;AACF"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type Theme } from '@mui/material/styles';
|
|
18
|
+
/**
|
|
19
|
+
* Stripe Elements' `appearance`, built from the RESOLVED MUI theme.
|
|
20
|
+
*
|
|
21
|
+
* The card form is Stripe's iframe sitting in the middle of our own card,
|
|
22
|
+
* between our own `TextField`s. If its fields are a different height, radius,
|
|
23
|
+
* font or border weight, the seam is the thing the customer notices — which
|
|
24
|
+
* is the whole objection inline Elements exist to answer. So every value here
|
|
25
|
+
* is read off the theme, and none is written down: a hardcoded palette copy
|
|
26
|
+
* stops matching the first time the scheme moves, and nothing would fail.
|
|
27
|
+
*
|
|
28
|
+
* ## Why CSS variables have to be resolved first
|
|
29
|
+
*
|
|
30
|
+
* The console's theme is served through MUI's `cssVariables` surfaces, so
|
|
31
|
+
* `theme.palette.primary.main` can come back as the STRING
|
|
32
|
+
* `'var(--mui-palette-primary-main)'` rather than a color. That string is
|
|
33
|
+
* fine in our own DOM and useless to Stripe: Elements paints inside a
|
|
34
|
+
* cross-origin iframe on Stripe's domain, where our custom properties do not
|
|
35
|
+
* exist. The variable resolves to nothing, and the field renders with
|
|
36
|
+
* Stripe's default — silently, on a surface nobody re-checks after a theme
|
|
37
|
+
* change.
|
|
38
|
+
*
|
|
39
|
+
* `resolveCssVar` is therefore not a nicety. It reads the computed value off
|
|
40
|
+
* the document once, so what crosses the iframe boundary is always a literal
|
|
41
|
+
* color.
|
|
42
|
+
*/
|
|
43
|
+
/** Reads a custom property's computed value from the document root. */
|
|
44
|
+
export type CssVarResolver = (name: string) => string;
|
|
45
|
+
/**
|
|
46
|
+
* Turn whatever the theme handed us into a literal a cross-origin iframe can
|
|
47
|
+
* paint with.
|
|
48
|
+
*
|
|
49
|
+
* Handles the nested form MUI emits — `var(--a, var(--b, #fff))` — by
|
|
50
|
+
* resolving outside-in and falling back to the declared default, which is
|
|
51
|
+
* what the browser itself would do.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveThemeColor(value: unknown, resolveCssVar?: CssVarResolver, depth?: number): string;
|
|
54
|
+
/** The subset of Stripe's appearance object this builds. */
|
|
55
|
+
export interface StripeAppearance {
|
|
56
|
+
theme: 'stripe' | 'night';
|
|
57
|
+
variables: Record<string, string>;
|
|
58
|
+
rules: Record<string, Record<string, string>>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Build the appearance for one resolved MUI theme.
|
|
62
|
+
*
|
|
63
|
+
* `theme: 'night'` for a dark scheme rather than only recoloring: Stripe's
|
|
64
|
+
* base theme decides things the variables do not reach — icon artwork, the
|
|
65
|
+
* dropdown surface, autofill styling — and a light base wearing dark colors
|
|
66
|
+
* shows through on exactly those.
|
|
67
|
+
*
|
|
68
|
+
* ## The scheme comes off the resolved surface
|
|
69
|
+
*
|
|
70
|
+
* On a `cssVariables` theme, `theme.palette` is the DEFAULT scheme whichever
|
|
71
|
+
* one is showing: `useTheme()` hands the light palette to a page painted dark,
|
|
72
|
+
* and the fields came out white on a dark panel with their labels unreadable.
|
|
73
|
+
* `theme.vars.palette` holds the variables that do change, so those are what
|
|
74
|
+
* gets resolved, and the base theme is read from the surface they resolve to.
|
|
75
|
+
*/
|
|
76
|
+
export declare function stripeAppearanceFromTheme(theme: Theme, resolveCssVar?: CssVarResolver): StripeAppearance;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { getLuminance } from "@mui/material/styles";
|
|
17
|
+
const documentResolver = (name)=>{
|
|
18
|
+
if (typeof document === 'undefined') return '';
|
|
19
|
+
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Turn whatever the theme handed us into a literal a cross-origin iframe can
|
|
23
|
+
* paint with.
|
|
24
|
+
*
|
|
25
|
+
* Handles the nested form MUI emits — `var(--a, var(--b, #fff))` — by
|
|
26
|
+
* resolving outside-in and falling back to the declared default, which is
|
|
27
|
+
* what the browser itself would do.
|
|
28
|
+
*/ export function resolveThemeColor(value, resolveCssVar = documentResolver, depth = 0) {
|
|
29
|
+
const raw = String(value != null ? value : '').trim();
|
|
30
|
+
if (!raw) return '';
|
|
31
|
+
// Bounded: a malformed theme could otherwise loop on a self-referential var.
|
|
32
|
+
if (depth > 4) return '';
|
|
33
|
+
if (!raw.startsWith('var(')) return raw;
|
|
34
|
+
const inner = raw.slice('var('.length, raw.lastIndexOf(')'));
|
|
35
|
+
const comma = inner.indexOf(',');
|
|
36
|
+
const name = (comma === -1 ? inner : inner.slice(0, comma)).trim();
|
|
37
|
+
const fallback = comma === -1 ? '' : inner.slice(comma + 1).trim();
|
|
38
|
+
const resolved = resolveCssVar(name);
|
|
39
|
+
if (resolved) return resolveThemeColor(resolved, resolveCssVar, depth + 1);
|
|
40
|
+
return fallback ? resolveThemeColor(fallback, resolveCssVar, depth + 1) : '';
|
|
41
|
+
}
|
|
42
|
+
/** Whether a resolved surface color reads as dark, or `fallback` when it cannot be read. */ function surfaceIsDark(surface, fallback) {
|
|
43
|
+
try {
|
|
44
|
+
return getLuminance(surface) < 0.5;
|
|
45
|
+
} catch (unused) {
|
|
46
|
+
return fallback;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build the appearance for one resolved MUI theme.
|
|
51
|
+
*
|
|
52
|
+
* `theme: 'night'` for a dark scheme rather than only recoloring: Stripe's
|
|
53
|
+
* base theme decides things the variables do not reach — icon artwork, the
|
|
54
|
+
* dropdown surface, autofill styling — and a light base wearing dark colors
|
|
55
|
+
* shows through on exactly those.
|
|
56
|
+
*
|
|
57
|
+
* ## The scheme comes off the resolved surface
|
|
58
|
+
*
|
|
59
|
+
* On a `cssVariables` theme, `theme.palette` is the DEFAULT scheme whichever
|
|
60
|
+
* one is showing: `useTheme()` hands the light palette to a page painted dark,
|
|
61
|
+
* and the fields came out white on a dark panel with their labels unreadable.
|
|
62
|
+
* `theme.vars.palette` holds the variables that do change, so those are what
|
|
63
|
+
* gets resolved, and the base theme is read from the surface they resolve to.
|
|
64
|
+
*/ export function stripeAppearanceFromTheme(theme, resolveCssVar = documentResolver) {
|
|
65
|
+
var _ref, _ref1, _ref2, _ref3;
|
|
66
|
+
var _theme_vars, _theme_shape, _theme_shape1, _palette_background, _palette_primary, _palette_background1, _palette_text, _palette_text1, _palette_text2, _palette_error, _palette_success, _theme_typography, _theme_typography_body1, _theme_typography1, _palette_text3, _palette_primary1, _palette_error1, _palette_text4;
|
|
67
|
+
const color = (value)=>resolveThemeColor(value, resolveCssVar);
|
|
68
|
+
const palette = (_ref = (_theme_vars = theme.vars) == null ? void 0 : _theme_vars.palette) != null ? _ref : theme.palette;
|
|
69
|
+
const radius = typeof ((_theme_shape = theme.shape) == null ? void 0 : _theme_shape.borderRadius) === 'number' ? `${theme.shape.borderRadius}px` : String((_ref1 = (_theme_shape1 = theme.shape) == null ? void 0 : _theme_shape1.borderRadius) != null ? _ref1 : '4px');
|
|
70
|
+
const dark = surfaceIsDark(color((_palette_background = palette.background) == null ? void 0 : _palette_background.paper), theme.palette.mode === 'dark');
|
|
71
|
+
return {
|
|
72
|
+
theme: dark ? 'night' : 'stripe',
|
|
73
|
+
variables: {
|
|
74
|
+
colorPrimary: color((_palette_primary = palette.primary) == null ? void 0 : _palette_primary.main),
|
|
75
|
+
// The card's own surface, not the page's — Elements sits inside a
|
|
76
|
+
// `CardDisplay`, which is `background.paper`.
|
|
77
|
+
colorBackground: color((_palette_background1 = palette.background) == null ? void 0 : _palette_background1.paper),
|
|
78
|
+
colorText: color((_palette_text = palette.text) == null ? void 0 : _palette_text.primary),
|
|
79
|
+
colorTextSecondary: color((_palette_text1 = palette.text) == null ? void 0 : _palette_text1.secondary),
|
|
80
|
+
colorTextPlaceholder: color((_palette_text2 = palette.text) == null ? void 0 : _palette_text2.disabled),
|
|
81
|
+
colorDanger: color((_palette_error = palette.error) == null ? void 0 : _palette_error.main),
|
|
82
|
+
colorSuccess: color((_palette_success = palette.success) == null ? void 0 : _palette_success.main),
|
|
83
|
+
// The whole font stack, so a fallback face matches ours too.
|
|
84
|
+
fontFamily: String((_ref2 = (_theme_typography = theme.typography) == null ? void 0 : _theme_typography.fontFamily) != null ? _ref2 : ''),
|
|
85
|
+
fontSizeBase: String((_ref3 = (_theme_typography1 = theme.typography) == null ? void 0 : (_theme_typography_body1 = _theme_typography1.body1) == null ? void 0 : _theme_typography_body1.fontSize) != null ? _ref3 : '1rem'),
|
|
86
|
+
borderRadius: radius,
|
|
87
|
+
// Spacing is 8 here; Stripe multiplies its own scale by this.
|
|
88
|
+
spacingUnit: theme.spacing(0.5)
|
|
89
|
+
},
|
|
90
|
+
rules: {
|
|
91
|
+
'.Input': {
|
|
92
|
+
// The token that exists because MUI has none — see `PaletteOptions`.
|
|
93
|
+
border: `1px solid ${color(palette.inputOutline)}`,
|
|
94
|
+
boxShadow: 'none'
|
|
95
|
+
},
|
|
96
|
+
'.Input:hover': {
|
|
97
|
+
border: `1px solid ${color((_palette_text3 = palette.text) == null ? void 0 : _palette_text3.primary)}`
|
|
98
|
+
},
|
|
99
|
+
'.Input:focus': {
|
|
100
|
+
// MUI's outlined field thickens its border on focus rather than
|
|
101
|
+
// adding a glow; matching that is what keeps the two seamless.
|
|
102
|
+
border: `2px solid ${color((_palette_primary1 = palette.primary) == null ? void 0 : _palette_primary1.main)}`,
|
|
103
|
+
boxShadow: 'none',
|
|
104
|
+
outline: 'none'
|
|
105
|
+
},
|
|
106
|
+
'.Input--invalid': {
|
|
107
|
+
border: `1px solid ${color((_palette_error1 = palette.error) == null ? void 0 : _palette_error1.main)}`,
|
|
108
|
+
boxShadow: 'none'
|
|
109
|
+
},
|
|
110
|
+
'.Label': {
|
|
111
|
+
color: color((_palette_text4 = palette.text) == null ? void 0 : _palette_text4.secondary)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
//# sourceMappingURL=stripe-elements-appearance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/stripe/stripe-elements-appearance.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 { getLuminance, type Theme } from '@mui/material/styles'\n\n/**\n * Stripe Elements' `appearance`, built from the RESOLVED MUI theme.\n *\n * The card form is Stripe's iframe sitting in the middle of our own card,\n * between our own `TextField`s. If its fields are a different height, radius,\n * font or border weight, the seam is the thing the customer notices — which\n * is the whole objection inline Elements exist to answer. So every value here\n * is read off the theme, and none is written down: a hardcoded palette copy\n * stops matching the first time the scheme moves, and nothing would fail.\n *\n * ## Why CSS variables have to be resolved first\n *\n * The console's theme is served through MUI's `cssVariables` surfaces, so\n * `theme.palette.primary.main` can come back as the STRING\n * `'var(--mui-palette-primary-main)'` rather than a color. That string is\n * fine in our own DOM and useless to Stripe: Elements paints inside a\n * cross-origin iframe on Stripe's domain, where our custom properties do not\n * exist. The variable resolves to nothing, and the field renders with\n * Stripe's default — silently, on a surface nobody re-checks after a theme\n * change.\n *\n * `resolveCssVar` is therefore not a nicety. It reads the computed value off\n * the document once, so what crosses the iframe boundary is always a literal\n * color.\n */\n\n/** Reads a custom property's computed value from the document root. */\nexport type CssVarResolver = (name: string) => string\n\nconst documentResolver: CssVarResolver = (name) => {\n if (typeof document === 'undefined') return ''\n return getComputedStyle(document.documentElement)\n .getPropertyValue(name)\n .trim()\n}\n\n/**\n * Turn whatever the theme handed us into a literal a cross-origin iframe can\n * paint with.\n *\n * Handles the nested form MUI emits — `var(--a, var(--b, #fff))` — by\n * resolving outside-in and falling back to the declared default, which is\n * what the browser itself would do.\n */\nexport function resolveThemeColor(\n value: unknown,\n resolveCssVar: CssVarResolver = documentResolver,\n depth = 0,\n): string {\n const raw = String(value ?? '').trim()\n if (!raw) return ''\n // Bounded: a malformed theme could otherwise loop on a self-referential var.\n if (depth > 4) return ''\n if (!raw.startsWith('var(')) return raw\n const inner = raw.slice('var('.length, raw.lastIndexOf(')'))\n const comma = inner.indexOf(',')\n const name = (comma === -1 ? inner : inner.slice(0, comma)).trim()\n const fallback = comma === -1 ? '' : inner.slice(comma + 1).trim()\n const resolved = resolveCssVar(name)\n if (resolved) return resolveThemeColor(resolved, resolveCssVar, depth + 1)\n return fallback ? resolveThemeColor(fallback, resolveCssVar, depth + 1) : ''\n}\n\n/** Whether a resolved surface color reads as dark, or `fallback` when it cannot be read. */\nfunction surfaceIsDark(surface: string, fallback: boolean): boolean {\n try {\n return getLuminance(surface) < 0.5\n } catch {\n return fallback\n }\n}\n\n/** The subset of Stripe's appearance object this builds. */\nexport interface StripeAppearance {\n theme: 'stripe' | 'night'\n variables: Record<string, string>\n rules: Record<string, Record<string, string>>\n}\n\n/**\n * Build the appearance for one resolved MUI theme.\n *\n * `theme: 'night'` for a dark scheme rather than only recoloring: Stripe's\n * base theme decides things the variables do not reach — icon artwork, the\n * dropdown surface, autofill styling — and a light base wearing dark colors\n * shows through on exactly those.\n *\n * ## The scheme comes off the resolved surface\n *\n * On a `cssVariables` theme, `theme.palette` is the DEFAULT scheme whichever\n * one is showing: `useTheme()` hands the light palette to a page painted dark,\n * and the fields came out white on a dark panel with their labels unreadable.\n * `theme.vars.palette` holds the variables that do change, so those are what\n * gets resolved, and the base theme is read from the surface they resolve to.\n */\nexport function stripeAppearanceFromTheme(\n theme: Theme,\n resolveCssVar: CssVarResolver = documentResolver,\n): StripeAppearance {\n const color = (value: unknown) => resolveThemeColor(value, resolveCssVar)\n const palette = (theme.vars?.palette ?? theme.palette) as Theme['palette']\n const radius =\n typeof theme.shape?.borderRadius === 'number'\n ? `${theme.shape.borderRadius}px`\n : String(theme.shape?.borderRadius ?? '4px')\n\n const dark = surfaceIsDark(color(palette.background?.paper), theme.palette.mode === 'dark')\n\n return {\n theme: dark ? 'night' : 'stripe',\n variables: {\n colorPrimary: color(palette.primary?.main),\n // The card's own surface, not the page's — Elements sits inside a\n // `CardDisplay`, which is `background.paper`.\n colorBackground: color(palette.background?.paper),\n colorText: color(palette.text?.primary),\n colorTextSecondary: color(palette.text?.secondary),\n colorTextPlaceholder: color(palette.text?.disabled),\n colorDanger: color(palette.error?.main),\n colorSuccess: color(palette.success?.main),\n // The whole font stack, so a fallback face matches ours too.\n fontFamily: String(theme.typography?.fontFamily ?? ''),\n fontSizeBase: String(theme.typography?.body1?.fontSize ?? '1rem'),\n borderRadius: radius,\n // Spacing is 8 here; Stripe multiplies its own scale by this.\n spacingUnit: theme.spacing(0.5),\n },\n rules: {\n '.Input': {\n // The token that exists because MUI has none — see `PaletteOptions`.\n border: `1px solid ${color(palette.inputOutline)}`,\n boxShadow: 'none',\n },\n '.Input:hover': {\n border: `1px solid ${color(palette.text?.primary)}`,\n },\n '.Input:focus': {\n // MUI's outlined field thickens its border on focus rather than\n // adding a glow; matching that is what keeps the two seamless.\n border: `2px solid ${color(palette.primary?.main)}`,\n boxShadow: 'none',\n outline: 'none',\n },\n '.Input--invalid': {\n border: `1px solid ${color(palette.error?.main)}`,\n boxShadow: 'none',\n },\n '.Label': {\n color: color(palette.text?.secondary),\n },\n },\n }\n}\n"],"names":["getLuminance","documentResolver","name","document","getComputedStyle","documentElement","getPropertyValue","trim","resolveThemeColor","value","resolveCssVar","depth","raw","String","startsWith","inner","slice","length","lastIndexOf","comma","indexOf","fallback","resolved","surfaceIsDark","surface","stripeAppearanceFromTheme","theme","palette","color","vars","radius","shape","borderRadius","dark","background","paper","mode","variables","colorPrimary","primary","main","colorBackground","colorText","text","colorTextSecondary","secondary","colorTextPlaceholder","disabled","colorDanger","error","colorSuccess","success","fontFamily","typography","fontSizeBase","body1","fontSize","spacingUnit","spacing","rules","border","inputOutline","boxShadow","outline"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,YAAY,QAAoB,uBAAsB;AA+B/D,MAAMC,mBAAmC,CAACC;IACxC,IAAI,OAAOC,aAAa,aAAa,OAAO;IAC5C,OAAOC,iBAAiBD,SAASE,eAAe,EAC7CC,gBAAgB,CAACJ,MACjBK,IAAI;AACT;AAEA;;;;;;;CAOC,GACD,OAAO,SAASC,kBACdC,KAAc,EACdC,gBAAgCT,gBAAgB,EAChDU,QAAQ,CAAC;IAET,MAAMC,MAAMC,OAAOJ,gBAAAA,QAAS,IAAIF,IAAI;IACpC,IAAI,CAACK,KAAK,OAAO;IACjB,6EAA6E;IAC7E,IAAID,QAAQ,GAAG,OAAO;IACtB,IAAI,CAACC,IAAIE,UAAU,CAAC,SAAS,OAAOF;IACpC,MAAMG,QAAQH,IAAII,KAAK,CAAC,OAAOC,MAAM,EAAEL,IAAIM,WAAW,CAAC;IACvD,MAAMC,QAAQJ,MAAMK,OAAO,CAAC;IAC5B,MAAMlB,OAAO,AAACiB,CAAAA,UAAU,CAAC,IAAIJ,QAAQA,MAAMC,KAAK,CAAC,GAAGG,MAAK,EAAGZ,IAAI;IAChE,MAAMc,WAAWF,UAAU,CAAC,IAAI,KAAKJ,MAAMC,KAAK,CAACG,QAAQ,GAAGZ,IAAI;IAChE,MAAMe,WAAWZ,cAAcR;IAC/B,IAAIoB,UAAU,OAAOd,kBAAkBc,UAAUZ,eAAeC,QAAQ;IACxE,OAAOU,WAAWb,kBAAkBa,UAAUX,eAAeC,QAAQ,KAAK;AAC5E;AAEA,0FAA0F,GAC1F,SAASY,cAAcC,OAAe,EAAEH,QAAiB;IACvD,IAAI;QACF,OAAOrB,aAAawB,WAAW;IACjC,EAAE,eAAM;QACN,OAAOH;IACT;AACF;AASA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASI,0BACdC,KAAY,EACZhB,gBAAgCT,gBAAgB;;QAG/ByB,aAERA,cAEIA,eAEoBC,qBAKTA,kBAGGA,sBACNA,eACSA,gBACEA,gBACTA,gBACCA,kBAEDD,mBACEA,yBAAAA,oBAYQC,gBAKAA,mBAKAA,iBAIdA;IAjDnB,MAAMC,QAAQ,CAACnB,QAAmBD,kBAAkBC,OAAOC;IAC3D,MAAMiB,mBAAWD,cAAAA,MAAMG,IAAI,qBAAVH,YAAYC,OAAO,mBAAID,MAAMC,OAAO;IACrD,MAAMG,SACJ,SAAOJ,eAAAA,MAAMK,KAAK,qBAAXL,aAAaM,YAAY,MAAK,WACjC,GAAGN,MAAMK,KAAK,CAACC,YAAY,CAAC,EAAE,CAAC,GAC/BnB,iBAAOa,gBAAAA,MAAMK,KAAK,qBAAXL,cAAaM,YAAY,oBAAI;IAE1C,MAAMC,OAAOV,cAAcK,OAAMD,sBAAAA,QAAQO,UAAU,qBAAlBP,oBAAoBQ,KAAK,GAAGT,MAAMC,OAAO,CAACS,IAAI,KAAK;IAEpF,OAAO;QACLV,OAAOO,OAAO,UAAU;QACxBI,WAAW;YACTC,cAAcV,OAAMD,mBAAAA,QAAQY,OAAO,qBAAfZ,iBAAiBa,IAAI;YACzC,kEAAkE;YAClE,8CAA8C;YAC9CC,iBAAiBb,OAAMD,uBAAAA,QAAQO,UAAU,qBAAlBP,qBAAoBQ,KAAK;YAChDO,WAAWd,OAAMD,gBAAAA,QAAQgB,IAAI,qBAAZhB,cAAcY,OAAO;YACtCK,oBAAoBhB,OAAMD,iBAAAA,QAAQgB,IAAI,qBAAZhB,eAAckB,SAAS;YACjDC,sBAAsBlB,OAAMD,iBAAAA,QAAQgB,IAAI,qBAAZhB,eAAcoB,QAAQ;YAClDC,aAAapB,OAAMD,iBAAAA,QAAQsB,KAAK,qBAAbtB,eAAea,IAAI;YACtCU,cAActB,OAAMD,mBAAAA,QAAQwB,OAAO,qBAAfxB,iBAAiBa,IAAI;YACzC,6DAA6D;YAC7DY,YAAYvC,iBAAOa,oBAAAA,MAAM2B,UAAU,qBAAhB3B,kBAAkB0B,UAAU,oBAAI;YACnDE,cAAczC,iBAAOa,qBAAAA,MAAM2B,UAAU,sBAAhB3B,0BAAAA,mBAAkB6B,KAAK,qBAAvB7B,wBAAyB8B,QAAQ,oBAAI;YAC1DxB,cAAcF;YACd,8DAA8D;YAC9D2B,aAAa/B,MAAMgC,OAAO,CAAC;QAC7B;QACAC,OAAO;YACL,UAAU;gBACR,qEAAqE;gBACrEC,QAAQ,CAAC,UAAU,EAAEhC,MAAMD,QAAQkC,YAAY,GAAG;gBAClDC,WAAW;YACb;YACA,gBAAgB;gBACdF,QAAQ,CAAC,UAAU,EAAEhC,OAAMD,iBAAAA,QAAQgB,IAAI,qBAAZhB,eAAcY,OAAO,GAAG;YACrD;YACA,gBAAgB;gBACd,gEAAgE;gBAChE,+DAA+D;gBAC/DqB,QAAQ,CAAC,UAAU,EAAEhC,OAAMD,oBAAAA,QAAQY,OAAO,qBAAfZ,kBAAiBa,IAAI,GAAG;gBACnDsB,WAAW;gBACXC,SAAS;YACX;YACA,mBAAmB;gBACjBH,QAAQ,CAAC,UAAU,EAAEhC,OAAMD,kBAAAA,QAAQsB,KAAK,qBAAbtB,gBAAea,IAAI,GAAG;gBACjDsB,WAAW;YACb;YACA,UAAU;gBACRlC,OAAOA,OAAMD,iBAAAA,QAAQgB,IAAI,qBAAZhB,eAAckB,SAAS;YACtC;QACF;IACF;AACF"}
|