@billkit-eu/react 0.1.0 → 0.2.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/CHANGELOG.md +6 -4
- package/README.md +45 -3
- package/dist/index.cjs +31 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -23
- package/dist/index.d.ts +61 -23
- package/dist/index.js +32 -16
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/BillKitProvider.tsx +9 -22
- package/src/CheckoutElement.tsx +37 -6
- package/src/PaymentMethodElement.tsx +24 -9
- package/src/index.tsx +5 -1
- package/src/useElement.ts +68 -7
package/CHANGELOG.md
CHANGED
|
@@ -16,10 +16,12 @@ package's changelog.
|
|
|
16
16
|
First public release.
|
|
17
17
|
|
|
18
18
|
### Added
|
|
19
|
-
- `<BillKitProvider>`: holds the
|
|
20
|
-
|
|
21
|
-
props that vary. Shaped after `@stripe/react-stripe-js` `<Elements>`,
|
|
22
|
-
is the API most teams are migrating from
|
|
19
|
+
- `<BillKitProvider>`: holds the shared element configuration (origin
|
|
20
|
+
overrides, logger) for the tree below it, so individual elements take only
|
|
21
|
+
the props that vary. Shaped after `@stripe/react-stripe-js` `<Elements>`,
|
|
22
|
+
which is the API most teams are migrating from — but with **no
|
|
23
|
+
`publishableKey`**: BillKit has no publishable-key concept, and elements
|
|
24
|
+
authenticate with the session's ephemeral `client_secret` instead.
|
|
23
25
|
- `<CheckoutElement/>` and `<PaymentMethodElement/>`: React wrappers over the
|
|
24
26
|
corresponding `@billkit-eu/js` mounts, with the iframe's lifecycle tied to
|
|
25
27
|
the component's.
|
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ Both components are SSR-safe. They render `null` on the server and on the first
|
|
|
67
67
|
| `onReady` | `() => void` | The iframe booted and loaded the session. |
|
|
68
68
|
| `onChange` | `(e: ChangeEvent) => void` | `e.complete` drives an external pay button. |
|
|
69
69
|
| `onSuccess` | `(e: SuccessEvent) => void` | Terminal success with no redirect. |
|
|
70
|
-
| `onError` | `(e: BillKitElementError) => void` | Any element or payment error. |
|
|
70
|
+
| `onError` | `(e: BillKitElementError) => void` | Any element or payment error. Codes: `payment_declined`, `load_timeout`, `unsafe_redirect`. |
|
|
71
71
|
| `onRedirect` | `(url: string) => boolean \| void` | Before the top window navigates for 3DS or iDEAL. Return `false` to navigate yourself. |
|
|
72
72
|
|
|
73
73
|
`<PaymentMethodElement/>` also requires `customerId`.
|
|
@@ -78,7 +78,47 @@ Both components are SSR-safe. They render `null` on the server and on the first
|
|
|
78
78
|
|
|
79
79
|
Pass inline arrow functions freely. Callbacks are read through a ref at event time, so a fresh closure on every render does not tear down a live payment iframe. The same holds for `logger`: an inline `logger={{...}}`, or switching one on mid-session, never triggers a remount.
|
|
80
80
|
|
|
81
|
-
Only `clientSecret`, `locale` and the origin overrides remount the element. That is deliberate, because a remount destroys an in-progress payment.
|
|
81
|
+
Only `clientSecret`, `customerId`, `locale` and the origin overrides remount the element. That is deliberate, because a remount destroys an in-progress payment — but `customerId` has to be in that list: the wallet's "set default" and "remove" actions act on whichever customer the iframe was initialised with, so a stale frame would point them at the wrong person.
|
|
82
|
+
|
|
83
|
+
## Your own pay button
|
|
84
|
+
|
|
85
|
+
Take a `ref` and call `submit()`. Gate the button on `onChange`'s `complete`, and re-enable it from `onError` — a declined card fires `onError({ code: "payment_declined" })` while the element shows its own retry panel, so it is the only signal that the attempt is over.
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { useRef, useState } from "react";
|
|
89
|
+
import { BillKitProvider, CheckoutElement, type BillKitElementRef } from "@billkit-eu/react";
|
|
90
|
+
|
|
91
|
+
function Checkout({ clientSecret }: { clientSecret: string }) {
|
|
92
|
+
const element = useRef<BillKitElementRef>(null);
|
|
93
|
+
const [complete, setComplete] = useState(false);
|
|
94
|
+
const [submitting, setSubmitting] = useState(false);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<BillKitProvider>
|
|
98
|
+
<CheckoutElement
|
|
99
|
+
ref={element}
|
|
100
|
+
clientSecret={clientSecret}
|
|
101
|
+
onChange={({ complete }) => setComplete(complete)}
|
|
102
|
+
onError={() => setSubmitting(false)}
|
|
103
|
+
onSuccess={({ sessionId }) => router.push(`/thanks?cs=${sessionId}`)}
|
|
104
|
+
/>
|
|
105
|
+
<button
|
|
106
|
+
disabled={!complete || submitting}
|
|
107
|
+
onClick={() => {
|
|
108
|
+
setSubmitting(true);
|
|
109
|
+
element.current?.submit();
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
Pay
|
|
113
|
+
</button>
|
|
114
|
+
</BillKitProvider>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The ref also exposes `updateTheme(tokens)` for imperative restyling; the declarative `theme` prop is hot-applied without a remount and is usually what you want.
|
|
120
|
+
|
|
121
|
+
`<PaymentMethodElement/>`'s ref exposes `updateTheme()` only — that element has no form to submit; its actions are per-row buttons inside the iframe.
|
|
82
122
|
|
|
83
123
|
## Content Security Policy
|
|
84
124
|
|
|
@@ -104,7 +144,7 @@ Never logged: the `clientSecret`, message payloads, or full redirect URLs. Only
|
|
|
104
144
|
|
|
105
145
|
The shapes line up, with one difference worth calling out.
|
|
106
146
|
|
|
107
|
-
|
|
147
|
+
There is **no `publishableKey`**. BillKit has no publishable-key concept; the API only mints secret keys (`sk_live_...` / `sk_test_...`), which must never reach a browser. Elements authenticate with the ephemeral `client_secret` your server gets from `POST /v1/checkout/sessions` with `ui_mode: "embedded"` — `<BillKitProvider>` takes no credential at all.
|
|
108
148
|
|
|
109
149
|
See the [migration guide](https://docs.billkit.eu/migration/elements/) for the full comparison.
|
|
110
150
|
|
|
@@ -118,6 +158,8 @@ import type {
|
|
|
118
158
|
ChangeEvent,
|
|
119
159
|
SuccessEvent,
|
|
120
160
|
BillKitElementError,
|
|
161
|
+
BillKitElementRef, // the imperative handle: { submit, updateTheme }
|
|
162
|
+
ThemeableElementRef, // <PaymentMethodElement/>'s: { updateTheme }
|
|
121
163
|
} from "@billkit-eu/react";
|
|
122
164
|
```
|
|
123
165
|
|
package/dist/index.cjs
CHANGED
|
@@ -7,15 +7,14 @@ var js = require('@billkit-eu/js');
|
|
|
7
7
|
// src/BillKitProvider.tsx
|
|
8
8
|
var BillKitContext = react.createContext(null);
|
|
9
9
|
function BillKitProvider({
|
|
10
|
-
publishableKey,
|
|
11
10
|
iframeOrigin,
|
|
12
11
|
apiBase,
|
|
13
12
|
logger,
|
|
14
13
|
children
|
|
15
14
|
}) {
|
|
16
15
|
const value = react.useMemo(
|
|
17
|
-
() => ({
|
|
18
|
-
[
|
|
16
|
+
() => ({ iframeOrigin, apiBase, logger }),
|
|
17
|
+
[iframeOrigin, apiBase, logger]
|
|
19
18
|
);
|
|
20
19
|
return /* @__PURE__ */ jsxRuntime.jsx(BillKitContext.Provider, { value, children });
|
|
21
20
|
}
|
|
@@ -65,26 +64,43 @@ function useElement(mount, props) {
|
|
|
65
64
|
handle.destroy();
|
|
66
65
|
handleRef.current = null;
|
|
67
66
|
};
|
|
68
|
-
}, [isClient, props.clientSecret, props.locale, iframeOrigin, apiBase]);
|
|
67
|
+
}, [isClient, props.clientSecret, props.customerId, props.locale, iframeOrigin, apiBase]);
|
|
69
68
|
react.useEffect(() => {
|
|
70
69
|
if (themeKey && handleRef.current && props.theme) {
|
|
71
70
|
handleRef.current.updateTheme(props.theme);
|
|
72
71
|
}
|
|
73
72
|
}, [themeKey]);
|
|
74
|
-
return { isClient, containerRef };
|
|
73
|
+
return { isClient, containerRef, handleRef };
|
|
75
74
|
}
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
function checkoutElementRef(handleRef) {
|
|
76
|
+
return {
|
|
77
|
+
submit: () => handleRef.current?.submit(),
|
|
78
|
+
updateTheme: (theme) => handleRef.current?.updateTheme(theme)
|
|
79
|
+
};
|
|
80
80
|
}
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (!isClient) return null;
|
|
86
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: props.className, style: props.style });
|
|
81
|
+
function themeableElementRef(handleRef) {
|
|
82
|
+
return {
|
|
83
|
+
updateTheme: (theme) => handleRef.current?.updateTheme(theme)
|
|
84
|
+
};
|
|
87
85
|
}
|
|
86
|
+
var CheckoutElement = react.forwardRef(
|
|
87
|
+
function CheckoutElement2(props, ref) {
|
|
88
|
+
const { isClient, containerRef, handleRef } = useElement(js.mountCheckoutElement, props);
|
|
89
|
+
react.useImperativeHandle(ref, () => checkoutElementRef(handleRef), []);
|
|
90
|
+
if (!isClient) return null;
|
|
91
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: props.className, style: props.style });
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
var PaymentMethodElement = react.forwardRef(
|
|
95
|
+
function PaymentMethodElement2(props, ref) {
|
|
96
|
+
const { customerId } = props;
|
|
97
|
+
const mount = (target, options) => js.mountPaymentMethodElement(target, { ...options, customerId });
|
|
98
|
+
const { isClient, containerRef, handleRef } = useElement(mount, props);
|
|
99
|
+
react.useImperativeHandle(ref, () => themeableElementRef(handleRef), []);
|
|
100
|
+
if (!isClient) return null;
|
|
101
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: props.className, style: props.style });
|
|
102
|
+
}
|
|
103
|
+
);
|
|
88
104
|
|
|
89
105
|
exports.BillKitProvider = BillKitProvider;
|
|
90
106
|
exports.CheckoutElement = CheckoutElement;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/BillKitProvider.tsx","../src/useElement.ts","../src/CheckoutElement.tsx","../src/PaymentMethodElement.tsx"],"names":["createContext","useMemo","jsx","useContext","useState","useRef","useEffect","mountCheckoutElement","mountPaymentMethodElement"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { BillKitElementLogger } from \"@billkit-eu/js\";\nimport { createContext, type ReactNode, useContext, useMemo } from \"react\";\n\n/**\n * Config shared by every BillKit element on the page. Mirrors\n * `@stripe/react-stripe-js`'s `<Elements>` provider: put your public key\n * (and any origin overrides) here once, then render elements freely\n * beneath it.\n */\nexport interface BillKitContextValue {\n /**\n * @deprecated Not required, not used, and not something BillKit issues.\n *\n * BillKit has no publishable-key concept; the API only mints secret\n * keys (`sk_live_...` / `sk_test_...`), which must never reach a browser.\n * Elements authenticate with the ephemeral `client_secret` your server\n * gets from `POST /v1/checkout/sessions` with `ui_mode: \"embedded\"`,\n * and that secret already names the tenant, the mode, and the session.\n *\n * The prop was accepted (and required) in 0.1.0 by analogy with\n * `@stripe/react-stripe-js`, but nothing ever read it, and a tenant\n * following the docs went looking for a `pk_...` value that does not\n * exist. It is now optional and ignored; pass nothing. It will be\n * removed in the next major.\n */\n publishableKey?: string;\n /** Origin the element iframe is served from. Defaults to js.billkit.eu. */\n iframeOrigin?: string;\n /** API origin the iframe calls. Defaults to api.billkit.eu. */\n apiBase?: string;\n /**\n * Where every element beneath this provider sends its lifecycle\n * diagnostics: iframe boot, dropped `postMessage`s, refused\n * redirects, load timeouts. Omitted (the default) means silence: the\n * elements never write to `console` on their own.\n *\n * `console` works as-is. An individual element can override this with\n * its own `logger` prop. The `clientSecret` is never passed to it.\n */\n logger?: BillKitElementLogger;\n}\n\nconst BillKitContext = createContext<BillKitContextValue | null>(null);\n\nexport interface BillKitProviderProps extends BillKitContextValue {\n children: ReactNode;\n}\n\n/**\n * Wrap the part of your tree that renders BillKit elements.\n *\n * No credential is needed here; the element authenticates with the\n * ephemeral `client_secret` your server minted for the session:\n *\n * ```tsx\n * <BillKitProvider>\n * <CheckoutElement clientSecret={clientSecret} onSuccess={done} />\n * </BillKitProvider>\n * ```\n */\nexport function BillKitProvider({\n publishableKey,\n iframeOrigin,\n apiBase,\n logger,\n children,\n}: BillKitProviderProps): JSX.Element {\n const value = useMemo<BillKitContextValue>(\n () => ({ publishableKey, iframeOrigin, apiBase, logger }),\n [publishableKey, iframeOrigin, apiBase, logger],\n );\n return <BillKitContext.Provider value={value}>{children}</BillKitContext.Provider>;\n}\n\n/**\n * Read the nearest {@link BillKitProvider}. Throws a clear error when an\n * element is rendered outside a provider, the most common integration\n * mistake.\n */\nexport function useBillKit(): BillKitContextValue {\n const ctx = useContext(BillKitContext);\n if (ctx === null) {\n throw new Error(\n \"BillKit: a <CheckoutElement/> must be rendered inside a <BillKitProvider>.\",\n );\n }\n return ctx;\n}\n","import type {\n BaseElementOptions,\n BillKitElementError,\n BillKitElementHandle,\n BillKitElementLogger,\n BillKitThemeTokens,\n ChangeEvent,\n SuccessEvent,\n} from \"@billkit-eu/js\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useBillKit } from \"./BillKitProvider\";\n\n/** Callback + presentational props common to the React element components. */\nexport interface ReactElementProps {\n /** Ephemeral checkout `client_secret` (`<sessionId>_secret_...`). */\n clientSecret: string;\n theme?: BillKitThemeTokens;\n locale?: string;\n /** Load-watchdog timeout (ms) before `onError({code:\"load_timeout\"})`. */\n loadTimeoutMs?: number;\n /**\n * Overrides the provider's `logger` for this element only. Omitted,\n * the provider's is used; with neither, the element is silent.\n */\n logger?: BillKitElementLogger;\n /** Applied to the container `<div>` the iframe mounts into. */\n className?: string;\n style?: React.CSSProperties;\n onReady?: () => void;\n onChange?: (event: ChangeEvent) => void;\n onSuccess?: (event: SuccessEvent) => void;\n onError?: (error: BillKitElementError) => void;\n onRedirect?: (url: string) => boolean | void;\n}\n\ntype MountFn = (\n target: HTMLElement,\n options: BaseElementOptions,\n) => BillKitElementHandle;\n\n/**\n * SSR-safe mount hook shared by `<CheckoutElement/>` and\n * `<PaymentMethodElement/>`.\n *\n * - Returns `isClient = false` on the server and on the first client\n * render, so the component renders `null` and hydration matches. The\n * `useEffect` then flips it true and the real mount happens, with no\n * hydration mismatch, no `window` access during render.\n * - Callbacks are read through a ref, so a parent passing fresh closures\n * every render never forces a costly iframe remount. Only the identity\n * inputs (secret, origins) remount; theme changes hot-update in place.\n */\nexport function useElement(\n mount: MountFn,\n props: ReactElementProps,\n): { isClient: boolean; containerRef: React.RefObject<HTMLDivElement> } {\n const { iframeOrigin, apiBase, logger: providerLogger } = useBillKit();\n const [isClient, setIsClient] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const handleRef = useRef<BillKitElementHandle | null>(null);\n\n // Latest callbacks, read at event time and decoupled from remount inputs.\n const callbacks = useRef(props);\n callbacks.current = props;\n\n // Same trick for the logger: resolve it at call time through a ref so\n // an inline `logger={{...}}` (or switching one on mid-session) never\n // tears down and rebuilds the payment iframe. The element always\n // receives this one stable object; it forwards to whatever is current,\n // or drops the line when neither prop nor provider supplies one.\n const activeLogger = useRef<BillKitElementLogger | undefined>(undefined);\n activeLogger.current = props.logger ?? providerLogger;\n const stableLogger = useRef<BillKitElementLogger>({\n debug: (message, context) => activeLogger.current?.debug(message, context),\n warn: (message, context) => activeLogger.current?.warn(message, context),\n }).current;\n\n useEffect(() => setIsClient(true), []);\n\n const themeKey = props.theme ? JSON.stringify(props.theme) : \"\";\n\n useEffect(() => {\n if (!isClient || containerRef.current === null) return;\n const options: BaseElementOptions = {\n clientSecret: props.clientSecret,\n ...(props.theme ? { theme: props.theme } : {}),\n ...(props.locale ? { locale: props.locale } : {}),\n ...(iframeOrigin ? { iframeOrigin } : {}),\n ...(apiBase ? { apiBase } : {}),\n ...(props.loadTimeoutMs !== undefined ? { loadTimeoutMs: props.loadTimeoutMs } : {}),\n logger: stableLogger,\n onReady: () => callbacks.current.onReady?.(),\n onChange: (e) => callbacks.current.onChange?.(e),\n onSuccess: (e) => callbacks.current.onSuccess?.(e),\n onError: (e) => callbacks.current.onError?.(e),\n onRedirect: (url) => callbacks.current.onRedirect?.(url),\n };\n const handle = mount(containerRef.current, options);\n handleRef.current = handle;\n return () => {\n handle.destroy();\n handleRef.current = null;\n };\n // Remount only on identity inputs (secret / origins); callbacks are\n // read through a ref and theme is hot-applied in the effect below, so\n // neither belongs in this dependency list.\n }, [isClient, props.clientSecret, props.locale, iframeOrigin, apiBase]);\n\n // Hot-apply theme changes without tearing down the iframe.\n useEffect(() => {\n if (themeKey && handleRef.current && props.theme) {\n handleRef.current.updateTheme(props.theme);\n }\n }, [themeKey]);\n\n return { isClient, containerRef };\n}\n","import { mountCheckoutElement } from \"@billkit-eu/js\";\nimport { type ReactElementProps, useElement } from \"./useElement\";\n\nexport type CheckoutElementProps = ReactElementProps;\n\n/**\n * Embedded checkout, as a React component. SSR-safe: renders `null` on the\n * server and the first client render, then mounts the js.billkit.eu iframe\n * after hydration.\n *\n * ```tsx\n * <BillKitProvider>\n * <CheckoutElement\n * clientSecret={clientSecret}\n * theme={{ colorPrimary: \"#6d28d9\", borderRadius: \"10px\" }}\n * onSuccess={({ sessionId }) => router.push(`/thanks?cs=${sessionId}`)}\n * />\n * </BillKitProvider>\n * ```\n */\nexport function CheckoutElement(props: CheckoutElementProps): JSX.Element | null {\n const { isClient, containerRef } = useElement(mountCheckoutElement, props);\n if (!isClient) return null;\n return <div ref={containerRef} className={props.className} style={props.style} />;\n}\n","import { type BaseElementOptions, type BillKitElementHandle, mountPaymentMethodElement } from \"@billkit-eu/js\";\nimport { type ReactElementProps, useElement } from \"./useElement\";\n\nexport interface PaymentMethodElementProps extends ReactElementProps {\n /** The customer whose saved payment methods to render + manage. */\n customerId: string;\n}\n\n/**\n * The customer's saved payment methods (\"Visa •••• 4242 · Update\"), as a\n * React component. Same SSR-safe mounting as {@link CheckoutElement}.\n */\nexport function PaymentMethodElement(props: PaymentMethodElementProps): JSX.Element | null {\n const { customerId } = props;\n const mount = (target: HTMLElement, options: BaseElementOptions): BillKitElementHandle =>\n mountPaymentMethodElement(target, { ...options, customerId });\n const { isClient, containerRef } = useElement(mount, props);\n if (!isClient) return null;\n return <div ref={containerRef} className={props.className} style={props.style} />;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/BillKitProvider.tsx","../src/useElement.ts","../src/CheckoutElement.tsx","../src/PaymentMethodElement.tsx"],"names":["createContext","useMemo","jsx","useContext","useState","useRef","useEffect","forwardRef","CheckoutElement","mountCheckoutElement","useImperativeHandle","PaymentMethodElement","mountPaymentMethodElement"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { BillKitElementLogger } from \"@billkit-eu/js\";\nimport { createContext, type ReactNode, useContext, useMemo } from \"react\";\n\n/**\n * Config shared by every BillKit element on the page. Mirrors\n * `@stripe/react-stripe-js`'s `<Elements>` provider — minus the key:\n * BillKit has no publishable-key concept. The API only mints secret keys\n * (`sk_live_...` / `sk_test_...`), which must never reach a browser;\n * elements authenticate with the ephemeral `client_secret` your server\n * gets from `POST /v1/checkout/sessions` with `ui_mode: \"embedded\"`, and\n * that secret already names the tenant, the mode, and the session. So the\n * provider carries origin overrides and a logger, nothing credential-shaped.\n */\nexport interface BillKitContextValue {\n /** Origin the element iframe is served from. Defaults to js.billkit.eu. */\n iframeOrigin?: string;\n /** API origin the iframe calls. Defaults to api.billkit.eu. */\n apiBase?: string;\n /**\n * Where every element beneath this provider sends its lifecycle\n * diagnostics: iframe boot, dropped `postMessage`s, refused\n * redirects, load timeouts. Omitted (the default) means silence: the\n * elements never write to `console` on their own.\n *\n * `console` works as-is. An individual element can override this with\n * its own `logger` prop. The `clientSecret` is never passed to it.\n */\n logger?: BillKitElementLogger;\n}\n\nconst BillKitContext = createContext<BillKitContextValue | null>(null);\n\nexport interface BillKitProviderProps extends BillKitContextValue {\n children: ReactNode;\n}\n\n/**\n * Wrap the part of your tree that renders BillKit elements.\n *\n * No credential is needed here; the element authenticates with the\n * ephemeral `client_secret` your server minted for the session:\n *\n * ```tsx\n * <BillKitProvider>\n * <CheckoutElement clientSecret={clientSecret} onSuccess={done} />\n * </BillKitProvider>\n * ```\n */\nexport function BillKitProvider({\n iframeOrigin,\n apiBase,\n logger,\n children,\n}: BillKitProviderProps): JSX.Element {\n const value = useMemo<BillKitContextValue>(\n () => ({ iframeOrigin, apiBase, logger }),\n [iframeOrigin, apiBase, logger],\n );\n return <BillKitContext.Provider value={value}>{children}</BillKitContext.Provider>;\n}\n\n/**\n * Read the nearest {@link BillKitProvider}. Throws a clear error when an\n * element is rendered outside a provider, the most common integration\n * mistake.\n */\nexport function useBillKit(): BillKitContextValue {\n const ctx = useContext(BillKitContext);\n if (ctx === null) {\n throw new Error(\n \"BillKit: a <CheckoutElement/> must be rendered inside a <BillKitProvider>.\",\n );\n }\n return ctx;\n}\n","import type {\n BaseElementOptions,\n BillKitElementError,\n BillKitElementHandle,\n BillKitElementLogger,\n BillKitThemeTokens,\n ChangeEvent,\n SuccessEvent,\n} from \"@billkit-eu/js\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useBillKit } from \"./BillKitProvider\";\n\n/** Callback + presentational props common to the React element components. */\nexport interface ReactElementProps {\n /** Ephemeral checkout `client_secret` (`<sessionId>_secret_...`). */\n clientSecret: string;\n theme?: BillKitThemeTokens;\n locale?: string;\n /** Load-watchdog timeout (ms) before `onError({code:\"load_timeout\"})`. */\n loadTimeoutMs?: number;\n /**\n * Overrides the provider's `logger` for this element only. Omitted,\n * the provider's is used; with neither, the element is silent.\n */\n logger?: BillKitElementLogger;\n /** Applied to the container `<div>` the iframe mounts into. */\n className?: string;\n style?: React.CSSProperties;\n onReady?: () => void;\n onChange?: (event: ChangeEvent) => void;\n onSuccess?: (event: SuccessEvent) => void;\n onError?: (error: BillKitElementError) => void;\n onRedirect?: (url: string) => boolean | void;\n}\n\ntype MountFn = (\n target: HTMLElement,\n options: BaseElementOptions,\n) => BillKitElementHandle;\n\n/**\n * SSR-safe mount hook shared by `<CheckoutElement/>` and\n * `<PaymentMethodElement/>`.\n *\n * - Returns `isClient = false` on the server and on the first client\n * render, so the component renders `null` and hydration matches. The\n * `useEffect` then flips it true and the real mount happens, with no\n * hydration mismatch, no `window` access during render.\n * - Callbacks are read through a ref, so a parent passing fresh closures\n * every render never forces a costly iframe remount. Only the identity\n * inputs (secret, origins) remount; theme changes hot-update in place.\n */\nexport function useElement(\n mount: MountFn,\n // `customerId` is not part of the public `ReactElementProps` (only the\n // payment-method element has one), but the hook still has to *see* it:\n // it is a remount input, and leaving it out of the dependency list\n // meant switching customers kept the previous customer's wallet — and\n // its \"set default\" / \"remove\" actions — on screen.\n props: ReactElementProps & { customerId?: string },\n): {\n isClient: boolean;\n containerRef: React.RefObject<HTMLDivElement>;\n handleRef: React.RefObject<BillKitElementHandle | null>;\n} {\n const { iframeOrigin, apiBase, logger: providerLogger } = useBillKit();\n const [isClient, setIsClient] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const handleRef = useRef<BillKitElementHandle | null>(null);\n\n // Latest callbacks, read at event time and decoupled from remount inputs.\n const callbacks = useRef(props);\n callbacks.current = props;\n\n // Same trick for the logger: resolve it at call time through a ref so\n // an inline `logger={{...}}` (or switching one on mid-session) never\n // tears down and rebuilds the payment iframe. The element always\n // receives this one stable object; it forwards to whatever is current,\n // or drops the line when neither prop nor provider supplies one.\n const activeLogger = useRef<BillKitElementLogger | undefined>(undefined);\n activeLogger.current = props.logger ?? providerLogger;\n const stableLogger = useRef<BillKitElementLogger>({\n debug: (message, context) => activeLogger.current?.debug(message, context),\n warn: (message, context) => activeLogger.current?.warn(message, context),\n }).current;\n\n useEffect(() => setIsClient(true), []);\n\n const themeKey = props.theme ? JSON.stringify(props.theme) : \"\";\n\n useEffect(() => {\n if (!isClient || containerRef.current === null) return;\n const options: BaseElementOptions = {\n clientSecret: props.clientSecret,\n ...(props.theme ? { theme: props.theme } : {}),\n ...(props.locale ? { locale: props.locale } : {}),\n ...(iframeOrigin ? { iframeOrigin } : {}),\n ...(apiBase ? { apiBase } : {}),\n ...(props.loadTimeoutMs !== undefined ? { loadTimeoutMs: props.loadTimeoutMs } : {}),\n logger: stableLogger,\n onReady: () => callbacks.current.onReady?.(),\n onChange: (e) => callbacks.current.onChange?.(e),\n onSuccess: (e) => callbacks.current.onSuccess?.(e),\n onError: (e) => callbacks.current.onError?.(e),\n onRedirect: (url) => callbacks.current.onRedirect?.(url),\n };\n const handle = mount(containerRef.current, options);\n handleRef.current = handle;\n return () => {\n handle.destroy();\n handleRef.current = null;\n };\n // Remount only on identity inputs (secret / customer / origins);\n // callbacks are read through a ref and theme is hot-applied in the\n // effect below, so neither belongs in this dependency list.\n }, [isClient, props.clientSecret, props.customerId, props.locale, iframeOrigin, apiBase]);\n\n // Hot-apply theme changes without tearing down the iframe.\n useEffect(() => {\n if (themeKey && handleRef.current && props.theme) {\n handleRef.current.updateTheme(props.theme);\n }\n }, [themeKey]);\n\n return { isClient, containerRef, handleRef };\n}\n\n/**\n * The imperative handle `<CheckoutElement/>` and\n * `<PaymentMethodElement/>` expose through `ref`.\n *\n * Everything else about these components is declarative, but submitting\n * is genuinely an *event*, not a state: a merchant's own pay button\n * lives outside the iframe (that is the point of `onChange.complete`),\n * and it has to be able to say \"go\" exactly once. A `submit` prop would\n * have to be a toggling boolean, which is the classic React smell for an\n * action modelled as state, so this follows the\n * `useImperativeHandle` path that `<input>`'s `focus()` set.\n *\n * Calls made before the iframe has mounted are no-ops rather than\n * throwing — on the server, and on the first client render, there is no\n * element yet.\n */\nexport interface ThemeableElementRef {\n /** Push new theme tokens in without remounting. */\n updateTheme(theme: BillKitThemeTokens): void;\n}\n\nexport interface BillKitElementRef extends ThemeableElementRef {\n /** Submit the form from your own pay button. */\n submit(): void;\n}\n\n/** Build the ref `<CheckoutElement/>` exposes. */\nexport function checkoutElementRef(\n handleRef: React.RefObject<BillKitElementHandle | null>,\n): BillKitElementRef {\n return {\n submit: () => handleRef.current?.submit(),\n updateTheme: (theme) => handleRef.current?.updateTheme(theme),\n };\n}\n\n/**\n * Build the ref `<PaymentMethodElement/>` exposes.\n *\n * Deliberately no `submit`: the wallet element drops `billkit:submit` on\n * the floor (there is no form to submit — its actions are per-row \"set\n * default\" / \"remove\" buttons inside the iframe). Exposing a method that\n * silently does nothing would be worse than not having one.\n */\nexport function themeableElementRef(\n handleRef: React.RefObject<BillKitElementHandle | null>,\n): ThemeableElementRef {\n return {\n updateTheme: (theme) => handleRef.current?.updateTheme(theme),\n };\n}\n","import { mountCheckoutElement } from \"@billkit-eu/js\";\nimport { forwardRef, useImperativeHandle } from \"react\";\nimport {\n type BillKitElementRef,\n checkoutElementRef,\n type ReactElementProps,\n useElement,\n} from \"./useElement\";\n\nexport type CheckoutElementProps = ReactElementProps;\n\n/**\n * Embedded checkout, as a React component. SSR-safe: renders `null` on the\n * server and the first client render, then mounts the js.billkit.eu iframe\n * after hydration.\n *\n * ```tsx\n * <BillKitProvider>\n * <CheckoutElement\n * clientSecret={clientSecret}\n * theme={{ colorPrimary: \"#6d28d9\", borderRadius: \"10px\" }}\n * onSuccess={({ sessionId }) => router.push(`/thanks?cs=${sessionId}`)}\n * />\n * </BillKitProvider>\n * ```\n *\n * Driving your own pay button: take a `ref` and call `submit()`. Gate the\n * button on `onChange`'s `complete`, and re-enable it from `onError` —\n * a declined card fires `onError({ code: \"payment_declined\" })` and the\n * element shows its own retry panel.\n *\n * ```tsx\n * const element = useRef<BillKitElementRef>(null);\n * const [ready, setReady] = useState(false);\n * <>\n * <CheckoutElement\n * ref={element}\n * clientSecret={clientSecret}\n * onChange={({ complete }) => setReady(complete)}\n * onError={() => setSubmitting(false)}\n * />\n * <button disabled={!ready} onClick={() => element.current?.submit()}>Pay</button>\n * </>\n * ```\n */\nexport const CheckoutElement = forwardRef<BillKitElementRef, CheckoutElementProps>(\n function CheckoutElement(props, ref) {\n const { isClient, containerRef, handleRef } = useElement(mountCheckoutElement, props);\n // No dependency list: `handleRef` is a stable ref object, and the\n // closures read `.current` at call time, so the imperative handle\n // never goes stale across remounts.\n useImperativeHandle(ref, () => checkoutElementRef(handleRef), []);\n if (!isClient) return null;\n return <div ref={containerRef} className={props.className} style={props.style} />;\n },\n);\n","import { type BaseElementOptions, type BillKitElementHandle, mountPaymentMethodElement } from \"@billkit-eu/js\";\nimport { forwardRef, useImperativeHandle } from \"react\";\nimport {\n type ReactElementProps,\n type ThemeableElementRef,\n themeableElementRef,\n useElement,\n} from \"./useElement\";\n\nexport interface PaymentMethodElementProps extends ReactElementProps {\n /** The customer whose saved payment methods to render + manage. */\n customerId: string;\n}\n\n/**\n * The customer's saved payment methods (\"Visa •••• 4242 · Update\"), as a\n * React component. Same SSR-safe mounting as {@link CheckoutElement}.\n *\n * Changing `customerId` remounts the element, so the wallet on screen\n * always belongs to the customer named in the props.\n *\n * The `ref` exposes `updateTheme()` only — this element has no form to\n * submit; its actions are per-row buttons inside the iframe.\n */\nexport const PaymentMethodElement = forwardRef<ThemeableElementRef, PaymentMethodElementProps>(\n function PaymentMethodElement(props, ref) {\n const { customerId } = props;\n const mount = (target: HTMLElement, options: BaseElementOptions): BillKitElementHandle =>\n mountPaymentMethodElement(target, { ...options, customerId });\n const { isClient, containerRef, handleRef } = useElement(mount, props);\n useImperativeHandle(ref, () => themeableElementRef(handleRef), []);\n if (!isClient) return null;\n return <div ref={containerRef} className={props.className} style={props.style} />;\n },\n);\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,30 +1,19 @@
|
|
|
1
1
|
import { BillKitElementLogger, BillKitThemeTokens, ChangeEvent, SuccessEvent, BillKitElementError } from '@billkit-eu/js';
|
|
2
2
|
export { BillKitElementError, BillKitElementHandle, BillKitElementLogger, BillKitThemeTokens, ChangeEvent, ElementLogContext, SuccessEvent } from '@billkit-eu/js';
|
|
3
|
+
import * as react from 'react';
|
|
3
4
|
import { ReactNode } from 'react';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Config shared by every BillKit element on the page. Mirrors
|
|
7
|
-
* `@stripe/react-stripe-js`'s `<Elements>` provider
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* `@stripe/react-stripe-js`'s `<Elements>` provider — minus the key:
|
|
9
|
+
* BillKit has no publishable-key concept. The API only mints secret keys
|
|
10
|
+
* (`sk_live_...` / `sk_test_...`), which must never reach a browser;
|
|
11
|
+
* elements authenticate with the ephemeral `client_secret` your server
|
|
12
|
+
* gets from `POST /v1/checkout/sessions` with `ui_mode: "embedded"`, and
|
|
13
|
+
* that secret already names the tenant, the mode, and the session. So the
|
|
14
|
+
* provider carries origin overrides and a logger, nothing credential-shaped.
|
|
10
15
|
*/
|
|
11
16
|
interface BillKitContextValue {
|
|
12
|
-
/**
|
|
13
|
-
* @deprecated Not required, not used, and not something BillKit issues.
|
|
14
|
-
*
|
|
15
|
-
* BillKit has no publishable-key concept; the API only mints secret
|
|
16
|
-
* keys (`sk_live_...` / `sk_test_...`), which must never reach a browser.
|
|
17
|
-
* Elements authenticate with the ephemeral `client_secret` your server
|
|
18
|
-
* gets from `POST /v1/checkout/sessions` with `ui_mode: "embedded"`,
|
|
19
|
-
* and that secret already names the tenant, the mode, and the session.
|
|
20
|
-
*
|
|
21
|
-
* The prop was accepted (and required) in 0.1.0 by analogy with
|
|
22
|
-
* `@stripe/react-stripe-js`, but nothing ever read it, and a tenant
|
|
23
|
-
* following the docs went looking for a `pk_...` value that does not
|
|
24
|
-
* exist. It is now optional and ignored; pass nothing. It will be
|
|
25
|
-
* removed in the next major.
|
|
26
|
-
*/
|
|
27
|
-
publishableKey?: string;
|
|
28
17
|
/** Origin the element iframe is served from. Defaults to js.billkit.eu. */
|
|
29
18
|
iframeOrigin?: string;
|
|
30
19
|
/** API origin the iframe calls. Defaults to api.billkit.eu. */
|
|
@@ -55,7 +44,7 @@ interface BillKitProviderProps extends BillKitContextValue {
|
|
|
55
44
|
* </BillKitProvider>
|
|
56
45
|
* ```
|
|
57
46
|
*/
|
|
58
|
-
declare function BillKitProvider({
|
|
47
|
+
declare function BillKitProvider({ iframeOrigin, apiBase, logger, children, }: BillKitProviderProps): JSX.Element;
|
|
59
48
|
/**
|
|
60
49
|
* Read the nearest {@link BillKitProvider}. Throws a clear error when an
|
|
61
50
|
* element is rendered outside a provider, the most common integration
|
|
@@ -85,6 +74,30 @@ interface ReactElementProps {
|
|
|
85
74
|
onError?: (error: BillKitElementError) => void;
|
|
86
75
|
onRedirect?: (url: string) => boolean | void;
|
|
87
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* The imperative handle `<CheckoutElement/>` and
|
|
79
|
+
* `<PaymentMethodElement/>` expose through `ref`.
|
|
80
|
+
*
|
|
81
|
+
* Everything else about these components is declarative, but submitting
|
|
82
|
+
* is genuinely an *event*, not a state: a merchant's own pay button
|
|
83
|
+
* lives outside the iframe (that is the point of `onChange.complete`),
|
|
84
|
+
* and it has to be able to say "go" exactly once. A `submit` prop would
|
|
85
|
+
* have to be a toggling boolean, which is the classic React smell for an
|
|
86
|
+
* action modelled as state, so this follows the
|
|
87
|
+
* `useImperativeHandle` path that `<input>`'s `focus()` set.
|
|
88
|
+
*
|
|
89
|
+
* Calls made before the iframe has mounted are no-ops rather than
|
|
90
|
+
* throwing — on the server, and on the first client render, there is no
|
|
91
|
+
* element yet.
|
|
92
|
+
*/
|
|
93
|
+
interface ThemeableElementRef {
|
|
94
|
+
/** Push new theme tokens in without remounting. */
|
|
95
|
+
updateTheme(theme: BillKitThemeTokens): void;
|
|
96
|
+
}
|
|
97
|
+
interface BillKitElementRef extends ThemeableElementRef {
|
|
98
|
+
/** Submit the form from your own pay button. */
|
|
99
|
+
submit(): void;
|
|
100
|
+
}
|
|
88
101
|
|
|
89
102
|
type CheckoutElementProps = ReactElementProps;
|
|
90
103
|
/**
|
|
@@ -101,8 +114,27 @@ type CheckoutElementProps = ReactElementProps;
|
|
|
101
114
|
* />
|
|
102
115
|
* </BillKitProvider>
|
|
103
116
|
* ```
|
|
117
|
+
*
|
|
118
|
+
* Driving your own pay button: take a `ref` and call `submit()`. Gate the
|
|
119
|
+
* button on `onChange`'s `complete`, and re-enable it from `onError` —
|
|
120
|
+
* a declined card fires `onError({ code: "payment_declined" })` and the
|
|
121
|
+
* element shows its own retry panel.
|
|
122
|
+
*
|
|
123
|
+
* ```tsx
|
|
124
|
+
* const element = useRef<BillKitElementRef>(null);
|
|
125
|
+
* const [ready, setReady] = useState(false);
|
|
126
|
+
* <>
|
|
127
|
+
* <CheckoutElement
|
|
128
|
+
* ref={element}
|
|
129
|
+
* clientSecret={clientSecret}
|
|
130
|
+
* onChange={({ complete }) => setReady(complete)}
|
|
131
|
+
* onError={() => setSubmitting(false)}
|
|
132
|
+
* />
|
|
133
|
+
* <button disabled={!ready} onClick={() => element.current?.submit()}>Pay</button>
|
|
134
|
+
* </>
|
|
135
|
+
* ```
|
|
104
136
|
*/
|
|
105
|
-
declare
|
|
137
|
+
declare const CheckoutElement: react.ForwardRefExoticComponent<ReactElementProps & react.RefAttributes<BillKitElementRef>>;
|
|
106
138
|
|
|
107
139
|
interface PaymentMethodElementProps extends ReactElementProps {
|
|
108
140
|
/** The customer whose saved payment methods to render + manage. */
|
|
@@ -111,7 +143,13 @@ interface PaymentMethodElementProps extends ReactElementProps {
|
|
|
111
143
|
/**
|
|
112
144
|
* The customer's saved payment methods ("Visa •••• 4242 · Update"), as a
|
|
113
145
|
* React component. Same SSR-safe mounting as {@link CheckoutElement}.
|
|
146
|
+
*
|
|
147
|
+
* Changing `customerId` remounts the element, so the wallet on screen
|
|
148
|
+
* always belongs to the customer named in the props.
|
|
149
|
+
*
|
|
150
|
+
* The `ref` exposes `updateTheme()` only — this element has no form to
|
|
151
|
+
* submit; its actions are per-row buttons inside the iframe.
|
|
114
152
|
*/
|
|
115
|
-
declare
|
|
153
|
+
declare const PaymentMethodElement: react.ForwardRefExoticComponent<PaymentMethodElementProps & react.RefAttributes<ThemeableElementRef>>;
|
|
116
154
|
|
|
117
|
-
export { type BillKitContextValue, BillKitProvider, type BillKitProviderProps, CheckoutElement, type CheckoutElementProps, PaymentMethodElement, type PaymentMethodElementProps, type ReactElementProps, useBillKit };
|
|
155
|
+
export { type BillKitContextValue, type BillKitElementRef, BillKitProvider, type BillKitProviderProps, CheckoutElement, type CheckoutElementProps, PaymentMethodElement, type PaymentMethodElementProps, type ReactElementProps, type ThemeableElementRef, useBillKit };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,30 +1,19 @@
|
|
|
1
1
|
import { BillKitElementLogger, BillKitThemeTokens, ChangeEvent, SuccessEvent, BillKitElementError } from '@billkit-eu/js';
|
|
2
2
|
export { BillKitElementError, BillKitElementHandle, BillKitElementLogger, BillKitThemeTokens, ChangeEvent, ElementLogContext, SuccessEvent } from '@billkit-eu/js';
|
|
3
|
+
import * as react from 'react';
|
|
3
4
|
import { ReactNode } from 'react';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Config shared by every BillKit element on the page. Mirrors
|
|
7
|
-
* `@stripe/react-stripe-js`'s `<Elements>` provider
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* `@stripe/react-stripe-js`'s `<Elements>` provider — minus the key:
|
|
9
|
+
* BillKit has no publishable-key concept. The API only mints secret keys
|
|
10
|
+
* (`sk_live_...` / `sk_test_...`), which must never reach a browser;
|
|
11
|
+
* elements authenticate with the ephemeral `client_secret` your server
|
|
12
|
+
* gets from `POST /v1/checkout/sessions` with `ui_mode: "embedded"`, and
|
|
13
|
+
* that secret already names the tenant, the mode, and the session. So the
|
|
14
|
+
* provider carries origin overrides and a logger, nothing credential-shaped.
|
|
10
15
|
*/
|
|
11
16
|
interface BillKitContextValue {
|
|
12
|
-
/**
|
|
13
|
-
* @deprecated Not required, not used, and not something BillKit issues.
|
|
14
|
-
*
|
|
15
|
-
* BillKit has no publishable-key concept; the API only mints secret
|
|
16
|
-
* keys (`sk_live_...` / `sk_test_...`), which must never reach a browser.
|
|
17
|
-
* Elements authenticate with the ephemeral `client_secret` your server
|
|
18
|
-
* gets from `POST /v1/checkout/sessions` with `ui_mode: "embedded"`,
|
|
19
|
-
* and that secret already names the tenant, the mode, and the session.
|
|
20
|
-
*
|
|
21
|
-
* The prop was accepted (and required) in 0.1.0 by analogy with
|
|
22
|
-
* `@stripe/react-stripe-js`, but nothing ever read it, and a tenant
|
|
23
|
-
* following the docs went looking for a `pk_...` value that does not
|
|
24
|
-
* exist. It is now optional and ignored; pass nothing. It will be
|
|
25
|
-
* removed in the next major.
|
|
26
|
-
*/
|
|
27
|
-
publishableKey?: string;
|
|
28
17
|
/** Origin the element iframe is served from. Defaults to js.billkit.eu. */
|
|
29
18
|
iframeOrigin?: string;
|
|
30
19
|
/** API origin the iframe calls. Defaults to api.billkit.eu. */
|
|
@@ -55,7 +44,7 @@ interface BillKitProviderProps extends BillKitContextValue {
|
|
|
55
44
|
* </BillKitProvider>
|
|
56
45
|
* ```
|
|
57
46
|
*/
|
|
58
|
-
declare function BillKitProvider({
|
|
47
|
+
declare function BillKitProvider({ iframeOrigin, apiBase, logger, children, }: BillKitProviderProps): JSX.Element;
|
|
59
48
|
/**
|
|
60
49
|
* Read the nearest {@link BillKitProvider}. Throws a clear error when an
|
|
61
50
|
* element is rendered outside a provider, the most common integration
|
|
@@ -85,6 +74,30 @@ interface ReactElementProps {
|
|
|
85
74
|
onError?: (error: BillKitElementError) => void;
|
|
86
75
|
onRedirect?: (url: string) => boolean | void;
|
|
87
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* The imperative handle `<CheckoutElement/>` and
|
|
79
|
+
* `<PaymentMethodElement/>` expose through `ref`.
|
|
80
|
+
*
|
|
81
|
+
* Everything else about these components is declarative, but submitting
|
|
82
|
+
* is genuinely an *event*, not a state: a merchant's own pay button
|
|
83
|
+
* lives outside the iframe (that is the point of `onChange.complete`),
|
|
84
|
+
* and it has to be able to say "go" exactly once. A `submit` prop would
|
|
85
|
+
* have to be a toggling boolean, which is the classic React smell for an
|
|
86
|
+
* action modelled as state, so this follows the
|
|
87
|
+
* `useImperativeHandle` path that `<input>`'s `focus()` set.
|
|
88
|
+
*
|
|
89
|
+
* Calls made before the iframe has mounted are no-ops rather than
|
|
90
|
+
* throwing — on the server, and on the first client render, there is no
|
|
91
|
+
* element yet.
|
|
92
|
+
*/
|
|
93
|
+
interface ThemeableElementRef {
|
|
94
|
+
/** Push new theme tokens in without remounting. */
|
|
95
|
+
updateTheme(theme: BillKitThemeTokens): void;
|
|
96
|
+
}
|
|
97
|
+
interface BillKitElementRef extends ThemeableElementRef {
|
|
98
|
+
/** Submit the form from your own pay button. */
|
|
99
|
+
submit(): void;
|
|
100
|
+
}
|
|
88
101
|
|
|
89
102
|
type CheckoutElementProps = ReactElementProps;
|
|
90
103
|
/**
|
|
@@ -101,8 +114,27 @@ type CheckoutElementProps = ReactElementProps;
|
|
|
101
114
|
* />
|
|
102
115
|
* </BillKitProvider>
|
|
103
116
|
* ```
|
|
117
|
+
*
|
|
118
|
+
* Driving your own pay button: take a `ref` and call `submit()`. Gate the
|
|
119
|
+
* button on `onChange`'s `complete`, and re-enable it from `onError` —
|
|
120
|
+
* a declined card fires `onError({ code: "payment_declined" })` and the
|
|
121
|
+
* element shows its own retry panel.
|
|
122
|
+
*
|
|
123
|
+
* ```tsx
|
|
124
|
+
* const element = useRef<BillKitElementRef>(null);
|
|
125
|
+
* const [ready, setReady] = useState(false);
|
|
126
|
+
* <>
|
|
127
|
+
* <CheckoutElement
|
|
128
|
+
* ref={element}
|
|
129
|
+
* clientSecret={clientSecret}
|
|
130
|
+
* onChange={({ complete }) => setReady(complete)}
|
|
131
|
+
* onError={() => setSubmitting(false)}
|
|
132
|
+
* />
|
|
133
|
+
* <button disabled={!ready} onClick={() => element.current?.submit()}>Pay</button>
|
|
134
|
+
* </>
|
|
135
|
+
* ```
|
|
104
136
|
*/
|
|
105
|
-
declare
|
|
137
|
+
declare const CheckoutElement: react.ForwardRefExoticComponent<ReactElementProps & react.RefAttributes<BillKitElementRef>>;
|
|
106
138
|
|
|
107
139
|
interface PaymentMethodElementProps extends ReactElementProps {
|
|
108
140
|
/** The customer whose saved payment methods to render + manage. */
|
|
@@ -111,7 +143,13 @@ interface PaymentMethodElementProps extends ReactElementProps {
|
|
|
111
143
|
/**
|
|
112
144
|
* The customer's saved payment methods ("Visa •••• 4242 · Update"), as a
|
|
113
145
|
* React component. Same SSR-safe mounting as {@link CheckoutElement}.
|
|
146
|
+
*
|
|
147
|
+
* Changing `customerId` remounts the element, so the wallet on screen
|
|
148
|
+
* always belongs to the customer named in the props.
|
|
149
|
+
*
|
|
150
|
+
* The `ref` exposes `updateTheme()` only — this element has no form to
|
|
151
|
+
* submit; its actions are per-row buttons inside the iframe.
|
|
114
152
|
*/
|
|
115
|
-
declare
|
|
153
|
+
declare const PaymentMethodElement: react.ForwardRefExoticComponent<PaymentMethodElementProps & react.RefAttributes<ThemeableElementRef>>;
|
|
116
154
|
|
|
117
|
-
export { type BillKitContextValue, BillKitProvider, type BillKitProviderProps, CheckoutElement, type CheckoutElementProps, PaymentMethodElement, type PaymentMethodElementProps, type ReactElementProps, useBillKit };
|
|
155
|
+
export { type BillKitContextValue, type BillKitElementRef, BillKitProvider, type BillKitProviderProps, CheckoutElement, type CheckoutElementProps, PaymentMethodElement, type PaymentMethodElementProps, type ReactElementProps, type ThemeableElementRef, useBillKit };
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
import { createContext,
|
|
1
|
+
import { createContext, forwardRef, useImperativeHandle, useState, useRef, useEffect, useContext, useMemo } from 'react';
|
|
2
2
|
import { jsx } from 'react/jsx-runtime';
|
|
3
3
|
import { mountCheckoutElement, mountPaymentMethodElement } from '@billkit-eu/js';
|
|
4
4
|
|
|
5
5
|
// src/BillKitProvider.tsx
|
|
6
6
|
var BillKitContext = createContext(null);
|
|
7
7
|
function BillKitProvider({
|
|
8
|
-
publishableKey,
|
|
9
8
|
iframeOrigin,
|
|
10
9
|
apiBase,
|
|
11
10
|
logger,
|
|
12
11
|
children
|
|
13
12
|
}) {
|
|
14
13
|
const value = useMemo(
|
|
15
|
-
() => ({
|
|
16
|
-
[
|
|
14
|
+
() => ({ iframeOrigin, apiBase, logger }),
|
|
15
|
+
[iframeOrigin, apiBase, logger]
|
|
17
16
|
);
|
|
18
17
|
return /* @__PURE__ */ jsx(BillKitContext.Provider, { value, children });
|
|
19
18
|
}
|
|
@@ -63,26 +62,43 @@ function useElement(mount, props) {
|
|
|
63
62
|
handle.destroy();
|
|
64
63
|
handleRef.current = null;
|
|
65
64
|
};
|
|
66
|
-
}, [isClient, props.clientSecret, props.locale, iframeOrigin, apiBase]);
|
|
65
|
+
}, [isClient, props.clientSecret, props.customerId, props.locale, iframeOrigin, apiBase]);
|
|
67
66
|
useEffect(() => {
|
|
68
67
|
if (themeKey && handleRef.current && props.theme) {
|
|
69
68
|
handleRef.current.updateTheme(props.theme);
|
|
70
69
|
}
|
|
71
70
|
}, [themeKey]);
|
|
72
|
-
return { isClient, containerRef };
|
|
71
|
+
return { isClient, containerRef, handleRef };
|
|
73
72
|
}
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
function checkoutElementRef(handleRef) {
|
|
74
|
+
return {
|
|
75
|
+
submit: () => handleRef.current?.submit(),
|
|
76
|
+
updateTheme: (theme) => handleRef.current?.updateTheme(theme)
|
|
77
|
+
};
|
|
78
78
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!isClient) return null;
|
|
84
|
-
return /* @__PURE__ */ jsx("div", { ref: containerRef, className: props.className, style: props.style });
|
|
79
|
+
function themeableElementRef(handleRef) {
|
|
80
|
+
return {
|
|
81
|
+
updateTheme: (theme) => handleRef.current?.updateTheme(theme)
|
|
82
|
+
};
|
|
85
83
|
}
|
|
84
|
+
var CheckoutElement = forwardRef(
|
|
85
|
+
function CheckoutElement2(props, ref) {
|
|
86
|
+
const { isClient, containerRef, handleRef } = useElement(mountCheckoutElement, props);
|
|
87
|
+
useImperativeHandle(ref, () => checkoutElementRef(handleRef), []);
|
|
88
|
+
if (!isClient) return null;
|
|
89
|
+
return /* @__PURE__ */ jsx("div", { ref: containerRef, className: props.className, style: props.style });
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
var PaymentMethodElement = forwardRef(
|
|
93
|
+
function PaymentMethodElement2(props, ref) {
|
|
94
|
+
const { customerId } = props;
|
|
95
|
+
const mount = (target, options) => mountPaymentMethodElement(target, { ...options, customerId });
|
|
96
|
+
const { isClient, containerRef, handleRef } = useElement(mount, props);
|
|
97
|
+
useImperativeHandle(ref, () => themeableElementRef(handleRef), []);
|
|
98
|
+
if (!isClient) return null;
|
|
99
|
+
return /* @__PURE__ */ jsx("div", { ref: containerRef, className: props.className, style: props.style });
|
|
100
|
+
}
|
|
101
|
+
);
|
|
86
102
|
|
|
87
103
|
export { BillKitProvider, CheckoutElement, PaymentMethodElement, useBillKit };
|
|
88
104
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/BillKitProvider.tsx","../src/useElement.ts","../src/CheckoutElement.tsx","../src/PaymentMethodElement.tsx"],"names":["jsx"],"mappings":"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","file":"index.js","sourcesContent":["import type { BillKitElementLogger } from \"@billkit-eu/js\";\nimport { createContext, type ReactNode, useContext, useMemo } from \"react\";\n\n/**\n * Config shared by every BillKit element on the page. Mirrors\n * `@stripe/react-stripe-js`'s `<Elements>` provider: put your public key\n * (and any origin overrides) here once, then render elements freely\n * beneath it.\n */\nexport interface BillKitContextValue {\n /**\n * @deprecated Not required, not used, and not something BillKit issues.\n *\n * BillKit has no publishable-key concept; the API only mints secret\n * keys (`sk_live_...` / `sk_test_...`), which must never reach a browser.\n * Elements authenticate with the ephemeral `client_secret` your server\n * gets from `POST /v1/checkout/sessions` with `ui_mode: \"embedded\"`,\n * and that secret already names the tenant, the mode, and the session.\n *\n * The prop was accepted (and required) in 0.1.0 by analogy with\n * `@stripe/react-stripe-js`, but nothing ever read it, and a tenant\n * following the docs went looking for a `pk_...` value that does not\n * exist. It is now optional and ignored; pass nothing. It will be\n * removed in the next major.\n */\n publishableKey?: string;\n /** Origin the element iframe is served from. Defaults to js.billkit.eu. */\n iframeOrigin?: string;\n /** API origin the iframe calls. Defaults to api.billkit.eu. */\n apiBase?: string;\n /**\n * Where every element beneath this provider sends its lifecycle\n * diagnostics: iframe boot, dropped `postMessage`s, refused\n * redirects, load timeouts. Omitted (the default) means silence: the\n * elements never write to `console` on their own.\n *\n * `console` works as-is. An individual element can override this with\n * its own `logger` prop. The `clientSecret` is never passed to it.\n */\n logger?: BillKitElementLogger;\n}\n\nconst BillKitContext = createContext<BillKitContextValue | null>(null);\n\nexport interface BillKitProviderProps extends BillKitContextValue {\n children: ReactNode;\n}\n\n/**\n * Wrap the part of your tree that renders BillKit elements.\n *\n * No credential is needed here; the element authenticates with the\n * ephemeral `client_secret` your server minted for the session:\n *\n * ```tsx\n * <BillKitProvider>\n * <CheckoutElement clientSecret={clientSecret} onSuccess={done} />\n * </BillKitProvider>\n * ```\n */\nexport function BillKitProvider({\n publishableKey,\n iframeOrigin,\n apiBase,\n logger,\n children,\n}: BillKitProviderProps): JSX.Element {\n const value = useMemo<BillKitContextValue>(\n () => ({ publishableKey, iframeOrigin, apiBase, logger }),\n [publishableKey, iframeOrigin, apiBase, logger],\n );\n return <BillKitContext.Provider value={value}>{children}</BillKitContext.Provider>;\n}\n\n/**\n * Read the nearest {@link BillKitProvider}. Throws a clear error when an\n * element is rendered outside a provider, the most common integration\n * mistake.\n */\nexport function useBillKit(): BillKitContextValue {\n const ctx = useContext(BillKitContext);\n if (ctx === null) {\n throw new Error(\n \"BillKit: a <CheckoutElement/> must be rendered inside a <BillKitProvider>.\",\n );\n }\n return ctx;\n}\n","import type {\n BaseElementOptions,\n BillKitElementError,\n BillKitElementHandle,\n BillKitElementLogger,\n BillKitThemeTokens,\n ChangeEvent,\n SuccessEvent,\n} from \"@billkit-eu/js\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useBillKit } from \"./BillKitProvider\";\n\n/** Callback + presentational props common to the React element components. */\nexport interface ReactElementProps {\n /** Ephemeral checkout `client_secret` (`<sessionId>_secret_...`). */\n clientSecret: string;\n theme?: BillKitThemeTokens;\n locale?: string;\n /** Load-watchdog timeout (ms) before `onError({code:\"load_timeout\"})`. */\n loadTimeoutMs?: number;\n /**\n * Overrides the provider's `logger` for this element only. Omitted,\n * the provider's is used; with neither, the element is silent.\n */\n logger?: BillKitElementLogger;\n /** Applied to the container `<div>` the iframe mounts into. */\n className?: string;\n style?: React.CSSProperties;\n onReady?: () => void;\n onChange?: (event: ChangeEvent) => void;\n onSuccess?: (event: SuccessEvent) => void;\n onError?: (error: BillKitElementError) => void;\n onRedirect?: (url: string) => boolean | void;\n}\n\ntype MountFn = (\n target: HTMLElement,\n options: BaseElementOptions,\n) => BillKitElementHandle;\n\n/**\n * SSR-safe mount hook shared by `<CheckoutElement/>` and\n * `<PaymentMethodElement/>`.\n *\n * - Returns `isClient = false` on the server and on the first client\n * render, so the component renders `null` and hydration matches. The\n * `useEffect` then flips it true and the real mount happens, with no\n * hydration mismatch, no `window` access during render.\n * - Callbacks are read through a ref, so a parent passing fresh closures\n * every render never forces a costly iframe remount. Only the identity\n * inputs (secret, origins) remount; theme changes hot-update in place.\n */\nexport function useElement(\n mount: MountFn,\n props: ReactElementProps,\n): { isClient: boolean; containerRef: React.RefObject<HTMLDivElement> } {\n const { iframeOrigin, apiBase, logger: providerLogger } = useBillKit();\n const [isClient, setIsClient] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const handleRef = useRef<BillKitElementHandle | null>(null);\n\n // Latest callbacks, read at event time and decoupled from remount inputs.\n const callbacks = useRef(props);\n callbacks.current = props;\n\n // Same trick for the logger: resolve it at call time through a ref so\n // an inline `logger={{...}}` (or switching one on mid-session) never\n // tears down and rebuilds the payment iframe. The element always\n // receives this one stable object; it forwards to whatever is current,\n // or drops the line when neither prop nor provider supplies one.\n const activeLogger = useRef<BillKitElementLogger | undefined>(undefined);\n activeLogger.current = props.logger ?? providerLogger;\n const stableLogger = useRef<BillKitElementLogger>({\n debug: (message, context) => activeLogger.current?.debug(message, context),\n warn: (message, context) => activeLogger.current?.warn(message, context),\n }).current;\n\n useEffect(() => setIsClient(true), []);\n\n const themeKey = props.theme ? JSON.stringify(props.theme) : \"\";\n\n useEffect(() => {\n if (!isClient || containerRef.current === null) return;\n const options: BaseElementOptions = {\n clientSecret: props.clientSecret,\n ...(props.theme ? { theme: props.theme } : {}),\n ...(props.locale ? { locale: props.locale } : {}),\n ...(iframeOrigin ? { iframeOrigin } : {}),\n ...(apiBase ? { apiBase } : {}),\n ...(props.loadTimeoutMs !== undefined ? { loadTimeoutMs: props.loadTimeoutMs } : {}),\n logger: stableLogger,\n onReady: () => callbacks.current.onReady?.(),\n onChange: (e) => callbacks.current.onChange?.(e),\n onSuccess: (e) => callbacks.current.onSuccess?.(e),\n onError: (e) => callbacks.current.onError?.(e),\n onRedirect: (url) => callbacks.current.onRedirect?.(url),\n };\n const handle = mount(containerRef.current, options);\n handleRef.current = handle;\n return () => {\n handle.destroy();\n handleRef.current = null;\n };\n // Remount only on identity inputs (secret / origins); callbacks are\n // read through a ref and theme is hot-applied in the effect below, so\n // neither belongs in this dependency list.\n }, [isClient, props.clientSecret, props.locale, iframeOrigin, apiBase]);\n\n // Hot-apply theme changes without tearing down the iframe.\n useEffect(() => {\n if (themeKey && handleRef.current && props.theme) {\n handleRef.current.updateTheme(props.theme);\n }\n }, [themeKey]);\n\n return { isClient, containerRef };\n}\n","import { mountCheckoutElement } from \"@billkit-eu/js\";\nimport { type ReactElementProps, useElement } from \"./useElement\";\n\nexport type CheckoutElementProps = ReactElementProps;\n\n/**\n * Embedded checkout, as a React component. SSR-safe: renders `null` on the\n * server and the first client render, then mounts the js.billkit.eu iframe\n * after hydration.\n *\n * ```tsx\n * <BillKitProvider>\n * <CheckoutElement\n * clientSecret={clientSecret}\n * theme={{ colorPrimary: \"#6d28d9\", borderRadius: \"10px\" }}\n * onSuccess={({ sessionId }) => router.push(`/thanks?cs=${sessionId}`)}\n * />\n * </BillKitProvider>\n * ```\n */\nexport function CheckoutElement(props: CheckoutElementProps): JSX.Element | null {\n const { isClient, containerRef } = useElement(mountCheckoutElement, props);\n if (!isClient) return null;\n return <div ref={containerRef} className={props.className} style={props.style} />;\n}\n","import { type BaseElementOptions, type BillKitElementHandle, mountPaymentMethodElement } from \"@billkit-eu/js\";\nimport { type ReactElementProps, useElement } from \"./useElement\";\n\nexport interface PaymentMethodElementProps extends ReactElementProps {\n /** The customer whose saved payment methods to render + manage. */\n customerId: string;\n}\n\n/**\n * The customer's saved payment methods (\"Visa •••• 4242 · Update\"), as a\n * React component. Same SSR-safe mounting as {@link CheckoutElement}.\n */\nexport function PaymentMethodElement(props: PaymentMethodElementProps): JSX.Element | null {\n const { customerId } = props;\n const mount = (target: HTMLElement, options: BaseElementOptions): BillKitElementHandle =>\n mountPaymentMethodElement(target, { ...options, customerId });\n const { isClient, containerRef } = useElement(mount, props);\n if (!isClient) return null;\n return <div ref={containerRef} className={props.className} style={props.style} />;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/BillKitProvider.tsx","../src/useElement.ts","../src/CheckoutElement.tsx","../src/PaymentMethodElement.tsx"],"names":["CheckoutElement","jsx","forwardRef","PaymentMethodElement","useImperativeHandle"],"mappings":"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","file":"index.js","sourcesContent":["import type { BillKitElementLogger } from \"@billkit-eu/js\";\nimport { createContext, type ReactNode, useContext, useMemo } from \"react\";\n\n/**\n * Config shared by every BillKit element on the page. Mirrors\n * `@stripe/react-stripe-js`'s `<Elements>` provider — minus the key:\n * BillKit has no publishable-key concept. The API only mints secret keys\n * (`sk_live_...` / `sk_test_...`), which must never reach a browser;\n * elements authenticate with the ephemeral `client_secret` your server\n * gets from `POST /v1/checkout/sessions` with `ui_mode: \"embedded\"`, and\n * that secret already names the tenant, the mode, and the session. So the\n * provider carries origin overrides and a logger, nothing credential-shaped.\n */\nexport interface BillKitContextValue {\n /** Origin the element iframe is served from. Defaults to js.billkit.eu. */\n iframeOrigin?: string;\n /** API origin the iframe calls. Defaults to api.billkit.eu. */\n apiBase?: string;\n /**\n * Where every element beneath this provider sends its lifecycle\n * diagnostics: iframe boot, dropped `postMessage`s, refused\n * redirects, load timeouts. Omitted (the default) means silence: the\n * elements never write to `console` on their own.\n *\n * `console` works as-is. An individual element can override this with\n * its own `logger` prop. The `clientSecret` is never passed to it.\n */\n logger?: BillKitElementLogger;\n}\n\nconst BillKitContext = createContext<BillKitContextValue | null>(null);\n\nexport interface BillKitProviderProps extends BillKitContextValue {\n children: ReactNode;\n}\n\n/**\n * Wrap the part of your tree that renders BillKit elements.\n *\n * No credential is needed here; the element authenticates with the\n * ephemeral `client_secret` your server minted for the session:\n *\n * ```tsx\n * <BillKitProvider>\n * <CheckoutElement clientSecret={clientSecret} onSuccess={done} />\n * </BillKitProvider>\n * ```\n */\nexport function BillKitProvider({\n iframeOrigin,\n apiBase,\n logger,\n children,\n}: BillKitProviderProps): JSX.Element {\n const value = useMemo<BillKitContextValue>(\n () => ({ iframeOrigin, apiBase, logger }),\n [iframeOrigin, apiBase, logger],\n );\n return <BillKitContext.Provider value={value}>{children}</BillKitContext.Provider>;\n}\n\n/**\n * Read the nearest {@link BillKitProvider}. Throws a clear error when an\n * element is rendered outside a provider, the most common integration\n * mistake.\n */\nexport function useBillKit(): BillKitContextValue {\n const ctx = useContext(BillKitContext);\n if (ctx === null) {\n throw new Error(\n \"BillKit: a <CheckoutElement/> must be rendered inside a <BillKitProvider>.\",\n );\n }\n return ctx;\n}\n","import type {\n BaseElementOptions,\n BillKitElementError,\n BillKitElementHandle,\n BillKitElementLogger,\n BillKitThemeTokens,\n ChangeEvent,\n SuccessEvent,\n} from \"@billkit-eu/js\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useBillKit } from \"./BillKitProvider\";\n\n/** Callback + presentational props common to the React element components. */\nexport interface ReactElementProps {\n /** Ephemeral checkout `client_secret` (`<sessionId>_secret_...`). */\n clientSecret: string;\n theme?: BillKitThemeTokens;\n locale?: string;\n /** Load-watchdog timeout (ms) before `onError({code:\"load_timeout\"})`. */\n loadTimeoutMs?: number;\n /**\n * Overrides the provider's `logger` for this element only. Omitted,\n * the provider's is used; with neither, the element is silent.\n */\n logger?: BillKitElementLogger;\n /** Applied to the container `<div>` the iframe mounts into. */\n className?: string;\n style?: React.CSSProperties;\n onReady?: () => void;\n onChange?: (event: ChangeEvent) => void;\n onSuccess?: (event: SuccessEvent) => void;\n onError?: (error: BillKitElementError) => void;\n onRedirect?: (url: string) => boolean | void;\n}\n\ntype MountFn = (\n target: HTMLElement,\n options: BaseElementOptions,\n) => BillKitElementHandle;\n\n/**\n * SSR-safe mount hook shared by `<CheckoutElement/>` and\n * `<PaymentMethodElement/>`.\n *\n * - Returns `isClient = false` on the server and on the first client\n * render, so the component renders `null` and hydration matches. The\n * `useEffect` then flips it true and the real mount happens, with no\n * hydration mismatch, no `window` access during render.\n * - Callbacks are read through a ref, so a parent passing fresh closures\n * every render never forces a costly iframe remount. Only the identity\n * inputs (secret, origins) remount; theme changes hot-update in place.\n */\nexport function useElement(\n mount: MountFn,\n // `customerId` is not part of the public `ReactElementProps` (only the\n // payment-method element has one), but the hook still has to *see* it:\n // it is a remount input, and leaving it out of the dependency list\n // meant switching customers kept the previous customer's wallet — and\n // its \"set default\" / \"remove\" actions — on screen.\n props: ReactElementProps & { customerId?: string },\n): {\n isClient: boolean;\n containerRef: React.RefObject<HTMLDivElement>;\n handleRef: React.RefObject<BillKitElementHandle | null>;\n} {\n const { iframeOrigin, apiBase, logger: providerLogger } = useBillKit();\n const [isClient, setIsClient] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const handleRef = useRef<BillKitElementHandle | null>(null);\n\n // Latest callbacks, read at event time and decoupled from remount inputs.\n const callbacks = useRef(props);\n callbacks.current = props;\n\n // Same trick for the logger: resolve it at call time through a ref so\n // an inline `logger={{...}}` (or switching one on mid-session) never\n // tears down and rebuilds the payment iframe. The element always\n // receives this one stable object; it forwards to whatever is current,\n // or drops the line when neither prop nor provider supplies one.\n const activeLogger = useRef<BillKitElementLogger | undefined>(undefined);\n activeLogger.current = props.logger ?? providerLogger;\n const stableLogger = useRef<BillKitElementLogger>({\n debug: (message, context) => activeLogger.current?.debug(message, context),\n warn: (message, context) => activeLogger.current?.warn(message, context),\n }).current;\n\n useEffect(() => setIsClient(true), []);\n\n const themeKey = props.theme ? JSON.stringify(props.theme) : \"\";\n\n useEffect(() => {\n if (!isClient || containerRef.current === null) return;\n const options: BaseElementOptions = {\n clientSecret: props.clientSecret,\n ...(props.theme ? { theme: props.theme } : {}),\n ...(props.locale ? { locale: props.locale } : {}),\n ...(iframeOrigin ? { iframeOrigin } : {}),\n ...(apiBase ? { apiBase } : {}),\n ...(props.loadTimeoutMs !== undefined ? { loadTimeoutMs: props.loadTimeoutMs } : {}),\n logger: stableLogger,\n onReady: () => callbacks.current.onReady?.(),\n onChange: (e) => callbacks.current.onChange?.(e),\n onSuccess: (e) => callbacks.current.onSuccess?.(e),\n onError: (e) => callbacks.current.onError?.(e),\n onRedirect: (url) => callbacks.current.onRedirect?.(url),\n };\n const handle = mount(containerRef.current, options);\n handleRef.current = handle;\n return () => {\n handle.destroy();\n handleRef.current = null;\n };\n // Remount only on identity inputs (secret / customer / origins);\n // callbacks are read through a ref and theme is hot-applied in the\n // effect below, so neither belongs in this dependency list.\n }, [isClient, props.clientSecret, props.customerId, props.locale, iframeOrigin, apiBase]);\n\n // Hot-apply theme changes without tearing down the iframe.\n useEffect(() => {\n if (themeKey && handleRef.current && props.theme) {\n handleRef.current.updateTheme(props.theme);\n }\n }, [themeKey]);\n\n return { isClient, containerRef, handleRef };\n}\n\n/**\n * The imperative handle `<CheckoutElement/>` and\n * `<PaymentMethodElement/>` expose through `ref`.\n *\n * Everything else about these components is declarative, but submitting\n * is genuinely an *event*, not a state: a merchant's own pay button\n * lives outside the iframe (that is the point of `onChange.complete`),\n * and it has to be able to say \"go\" exactly once. A `submit` prop would\n * have to be a toggling boolean, which is the classic React smell for an\n * action modelled as state, so this follows the\n * `useImperativeHandle` path that `<input>`'s `focus()` set.\n *\n * Calls made before the iframe has mounted are no-ops rather than\n * throwing — on the server, and on the first client render, there is no\n * element yet.\n */\nexport interface ThemeableElementRef {\n /** Push new theme tokens in without remounting. */\n updateTheme(theme: BillKitThemeTokens): void;\n}\n\nexport interface BillKitElementRef extends ThemeableElementRef {\n /** Submit the form from your own pay button. */\n submit(): void;\n}\n\n/** Build the ref `<CheckoutElement/>` exposes. */\nexport function checkoutElementRef(\n handleRef: React.RefObject<BillKitElementHandle | null>,\n): BillKitElementRef {\n return {\n submit: () => handleRef.current?.submit(),\n updateTheme: (theme) => handleRef.current?.updateTheme(theme),\n };\n}\n\n/**\n * Build the ref `<PaymentMethodElement/>` exposes.\n *\n * Deliberately no `submit`: the wallet element drops `billkit:submit` on\n * the floor (there is no form to submit — its actions are per-row \"set\n * default\" / \"remove\" buttons inside the iframe). Exposing a method that\n * silently does nothing would be worse than not having one.\n */\nexport function themeableElementRef(\n handleRef: React.RefObject<BillKitElementHandle | null>,\n): ThemeableElementRef {\n return {\n updateTheme: (theme) => handleRef.current?.updateTheme(theme),\n };\n}\n","import { mountCheckoutElement } from \"@billkit-eu/js\";\nimport { forwardRef, useImperativeHandle } from \"react\";\nimport {\n type BillKitElementRef,\n checkoutElementRef,\n type ReactElementProps,\n useElement,\n} from \"./useElement\";\n\nexport type CheckoutElementProps = ReactElementProps;\n\n/**\n * Embedded checkout, as a React component. SSR-safe: renders `null` on the\n * server and the first client render, then mounts the js.billkit.eu iframe\n * after hydration.\n *\n * ```tsx\n * <BillKitProvider>\n * <CheckoutElement\n * clientSecret={clientSecret}\n * theme={{ colorPrimary: \"#6d28d9\", borderRadius: \"10px\" }}\n * onSuccess={({ sessionId }) => router.push(`/thanks?cs=${sessionId}`)}\n * />\n * </BillKitProvider>\n * ```\n *\n * Driving your own pay button: take a `ref` and call `submit()`. Gate the\n * button on `onChange`'s `complete`, and re-enable it from `onError` —\n * a declined card fires `onError({ code: \"payment_declined\" })` and the\n * element shows its own retry panel.\n *\n * ```tsx\n * const element = useRef<BillKitElementRef>(null);\n * const [ready, setReady] = useState(false);\n * <>\n * <CheckoutElement\n * ref={element}\n * clientSecret={clientSecret}\n * onChange={({ complete }) => setReady(complete)}\n * onError={() => setSubmitting(false)}\n * />\n * <button disabled={!ready} onClick={() => element.current?.submit()}>Pay</button>\n * </>\n * ```\n */\nexport const CheckoutElement = forwardRef<BillKitElementRef, CheckoutElementProps>(\n function CheckoutElement(props, ref) {\n const { isClient, containerRef, handleRef } = useElement(mountCheckoutElement, props);\n // No dependency list: `handleRef` is a stable ref object, and the\n // closures read `.current` at call time, so the imperative handle\n // never goes stale across remounts.\n useImperativeHandle(ref, () => checkoutElementRef(handleRef), []);\n if (!isClient) return null;\n return <div ref={containerRef} className={props.className} style={props.style} />;\n },\n);\n","import { type BaseElementOptions, type BillKitElementHandle, mountPaymentMethodElement } from \"@billkit-eu/js\";\nimport { forwardRef, useImperativeHandle } from \"react\";\nimport {\n type ReactElementProps,\n type ThemeableElementRef,\n themeableElementRef,\n useElement,\n} from \"./useElement\";\n\nexport interface PaymentMethodElementProps extends ReactElementProps {\n /** The customer whose saved payment methods to render + manage. */\n customerId: string;\n}\n\n/**\n * The customer's saved payment methods (\"Visa •••• 4242 · Update\"), as a\n * React component. Same SSR-safe mounting as {@link CheckoutElement}.\n *\n * Changing `customerId` remounts the element, so the wallet on screen\n * always belongs to the customer named in the props.\n *\n * The `ref` exposes `updateTheme()` only — this element has no form to\n * submit; its actions are per-row buttons inside the iframe.\n */\nexport const PaymentMethodElement = forwardRef<ThemeableElementRef, PaymentMethodElementProps>(\n function PaymentMethodElement(props, ref) {\n const { customerId } = props;\n const mount = (target: HTMLElement, options: BaseElementOptions): BillKitElementHandle =>\n mountPaymentMethodElement(target, { ...options, customerId });\n const { isClient, containerRef, handleRef } = useElement(mount, props);\n useImperativeHandle(ref, () => themeableElementRef(handleRef), []);\n if (!isClient) return null;\n return <div ref={containerRef} className={props.className} style={props.style} />;\n },\n);\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@billkit-eu/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "React bindings for BillKit's embedded checkout: <BillKitProvider> + <CheckoutElement/>, @stripe/react-stripe-js-shaped and SSR-safe.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -55,12 +55,12 @@
|
|
|
55
55
|
"provenance": true
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@billkit-eu/js": "
|
|
58
|
+
"@billkit-eu/js": ">=0.2.0 <1",
|
|
59
59
|
"react": ">=18",
|
|
60
60
|
"react-dom": ">=18"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@billkit-eu/js": "
|
|
63
|
+
"@billkit-eu/js": ">=0.2.0 <1",
|
|
64
64
|
"@testing-library/react": "^16.1.0",
|
|
65
65
|
"@types/node": "^26.1.1",
|
|
66
66
|
"@types/react": "^18.3.12",
|
package/src/BillKitProvider.tsx
CHANGED
|
@@ -3,27 +3,15 @@ import { createContext, type ReactNode, useContext, useMemo } from "react";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Config shared by every BillKit element on the page. Mirrors
|
|
6
|
-
* `@stripe/react-stripe-js`'s `<Elements>` provider
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* `@stripe/react-stripe-js`'s `<Elements>` provider — minus the key:
|
|
7
|
+
* BillKit has no publishable-key concept. The API only mints secret keys
|
|
8
|
+
* (`sk_live_...` / `sk_test_...`), which must never reach a browser;
|
|
9
|
+
* elements authenticate with the ephemeral `client_secret` your server
|
|
10
|
+
* gets from `POST /v1/checkout/sessions` with `ui_mode: "embedded"`, and
|
|
11
|
+
* that secret already names the tenant, the mode, and the session. So the
|
|
12
|
+
* provider carries origin overrides and a logger, nothing credential-shaped.
|
|
9
13
|
*/
|
|
10
14
|
export interface BillKitContextValue {
|
|
11
|
-
/**
|
|
12
|
-
* @deprecated Not required, not used, and not something BillKit issues.
|
|
13
|
-
*
|
|
14
|
-
* BillKit has no publishable-key concept; the API only mints secret
|
|
15
|
-
* keys (`sk_live_...` / `sk_test_...`), which must never reach a browser.
|
|
16
|
-
* Elements authenticate with the ephemeral `client_secret` your server
|
|
17
|
-
* gets from `POST /v1/checkout/sessions` with `ui_mode: "embedded"`,
|
|
18
|
-
* and that secret already names the tenant, the mode, and the session.
|
|
19
|
-
*
|
|
20
|
-
* The prop was accepted (and required) in 0.1.0 by analogy with
|
|
21
|
-
* `@stripe/react-stripe-js`, but nothing ever read it, and a tenant
|
|
22
|
-
* following the docs went looking for a `pk_...` value that does not
|
|
23
|
-
* exist. It is now optional and ignored; pass nothing. It will be
|
|
24
|
-
* removed in the next major.
|
|
25
|
-
*/
|
|
26
|
-
publishableKey?: string;
|
|
27
15
|
/** Origin the element iframe is served from. Defaults to js.billkit.eu. */
|
|
28
16
|
iframeOrigin?: string;
|
|
29
17
|
/** API origin the iframe calls. Defaults to api.billkit.eu. */
|
|
@@ -59,15 +47,14 @@ export interface BillKitProviderProps extends BillKitContextValue {
|
|
|
59
47
|
* ```
|
|
60
48
|
*/
|
|
61
49
|
export function BillKitProvider({
|
|
62
|
-
publishableKey,
|
|
63
50
|
iframeOrigin,
|
|
64
51
|
apiBase,
|
|
65
52
|
logger,
|
|
66
53
|
children,
|
|
67
54
|
}: BillKitProviderProps): JSX.Element {
|
|
68
55
|
const value = useMemo<BillKitContextValue>(
|
|
69
|
-
() => ({
|
|
70
|
-
[
|
|
56
|
+
() => ({ iframeOrigin, apiBase, logger }),
|
|
57
|
+
[iframeOrigin, apiBase, logger],
|
|
71
58
|
);
|
|
72
59
|
return <BillKitContext.Provider value={value}>{children}</BillKitContext.Provider>;
|
|
73
60
|
}
|
package/src/CheckoutElement.tsx
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { mountCheckoutElement } from "@billkit-eu/js";
|
|
2
|
-
import {
|
|
2
|
+
import { forwardRef, useImperativeHandle } from "react";
|
|
3
|
+
import {
|
|
4
|
+
type BillKitElementRef,
|
|
5
|
+
checkoutElementRef,
|
|
6
|
+
type ReactElementProps,
|
|
7
|
+
useElement,
|
|
8
|
+
} from "./useElement";
|
|
3
9
|
|
|
4
10
|
export type CheckoutElementProps = ReactElementProps;
|
|
5
11
|
|
|
@@ -17,9 +23,34 @@ export type CheckoutElementProps = ReactElementProps;
|
|
|
17
23
|
* />
|
|
18
24
|
* </BillKitProvider>
|
|
19
25
|
* ```
|
|
26
|
+
*
|
|
27
|
+
* Driving your own pay button: take a `ref` and call `submit()`. Gate the
|
|
28
|
+
* button on `onChange`'s `complete`, and re-enable it from `onError` —
|
|
29
|
+
* a declined card fires `onError({ code: "payment_declined" })` and the
|
|
30
|
+
* element shows its own retry panel.
|
|
31
|
+
*
|
|
32
|
+
* ```tsx
|
|
33
|
+
* const element = useRef<BillKitElementRef>(null);
|
|
34
|
+
* const [ready, setReady] = useState(false);
|
|
35
|
+
* <>
|
|
36
|
+
* <CheckoutElement
|
|
37
|
+
* ref={element}
|
|
38
|
+
* clientSecret={clientSecret}
|
|
39
|
+
* onChange={({ complete }) => setReady(complete)}
|
|
40
|
+
* onError={() => setSubmitting(false)}
|
|
41
|
+
* />
|
|
42
|
+
* <button disabled={!ready} onClick={() => element.current?.submit()}>Pay</button>
|
|
43
|
+
* </>
|
|
44
|
+
* ```
|
|
20
45
|
*/
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
46
|
+
export const CheckoutElement = forwardRef<BillKitElementRef, CheckoutElementProps>(
|
|
47
|
+
function CheckoutElement(props, ref) {
|
|
48
|
+
const { isClient, containerRef, handleRef } = useElement(mountCheckoutElement, props);
|
|
49
|
+
// No dependency list: `handleRef` is a stable ref object, and the
|
|
50
|
+
// closures read `.current` at call time, so the imperative handle
|
|
51
|
+
// never goes stale across remounts.
|
|
52
|
+
useImperativeHandle(ref, () => checkoutElementRef(handleRef), []);
|
|
53
|
+
if (!isClient) return null;
|
|
54
|
+
return <div ref={containerRef} className={props.className} style={props.style} />;
|
|
55
|
+
},
|
|
56
|
+
);
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { type BaseElementOptions, type BillKitElementHandle, mountPaymentMethodElement } from "@billkit-eu/js";
|
|
2
|
-
import {
|
|
2
|
+
import { forwardRef, useImperativeHandle } from "react";
|
|
3
|
+
import {
|
|
4
|
+
type ReactElementProps,
|
|
5
|
+
type ThemeableElementRef,
|
|
6
|
+
themeableElementRef,
|
|
7
|
+
useElement,
|
|
8
|
+
} from "./useElement";
|
|
3
9
|
|
|
4
10
|
export interface PaymentMethodElementProps extends ReactElementProps {
|
|
5
11
|
/** The customer whose saved payment methods to render + manage. */
|
|
@@ -9,12 +15,21 @@ export interface PaymentMethodElementProps extends ReactElementProps {
|
|
|
9
15
|
/**
|
|
10
16
|
* The customer's saved payment methods ("Visa •••• 4242 · Update"), as a
|
|
11
17
|
* React component. Same SSR-safe mounting as {@link CheckoutElement}.
|
|
18
|
+
*
|
|
19
|
+
* Changing `customerId` remounts the element, so the wallet on screen
|
|
20
|
+
* always belongs to the customer named in the props.
|
|
21
|
+
*
|
|
22
|
+
* The `ref` exposes `updateTheme()` only — this element has no form to
|
|
23
|
+
* submit; its actions are per-row buttons inside the iframe.
|
|
12
24
|
*/
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
export const PaymentMethodElement = forwardRef<ThemeableElementRef, PaymentMethodElementProps>(
|
|
26
|
+
function PaymentMethodElement(props, ref) {
|
|
27
|
+
const { customerId } = props;
|
|
28
|
+
const mount = (target: HTMLElement, options: BaseElementOptions): BillKitElementHandle =>
|
|
29
|
+
mountPaymentMethodElement(target, { ...options, customerId });
|
|
30
|
+
const { isClient, containerRef, handleRef } = useElement(mount, props);
|
|
31
|
+
useImperativeHandle(ref, () => themeableElementRef(handleRef), []);
|
|
32
|
+
if (!isClient) return null;
|
|
33
|
+
return <div ref={containerRef} className={props.className} style={props.style} />;
|
|
34
|
+
},
|
|
35
|
+
);
|
package/src/index.tsx
CHANGED
|
@@ -20,7 +20,11 @@ export {
|
|
|
20
20
|
PaymentMethodElement,
|
|
21
21
|
type PaymentMethodElementProps,
|
|
22
22
|
} from "./PaymentMethodElement";
|
|
23
|
-
export type {
|
|
23
|
+
export type {
|
|
24
|
+
BillKitElementRef,
|
|
25
|
+
ReactElementProps,
|
|
26
|
+
ThemeableElementRef,
|
|
27
|
+
} from "./useElement";
|
|
24
28
|
// Re-export the loader's shared types so consumers import from one place.
|
|
25
29
|
export type {
|
|
26
30
|
BillKitElementError,
|
package/src/useElement.ts
CHANGED
|
@@ -52,8 +52,17 @@ type MountFn = (
|
|
|
52
52
|
*/
|
|
53
53
|
export function useElement(
|
|
54
54
|
mount: MountFn,
|
|
55
|
-
|
|
56
|
-
)
|
|
55
|
+
// `customerId` is not part of the public `ReactElementProps` (only the
|
|
56
|
+
// payment-method element has one), but the hook still has to *see* it:
|
|
57
|
+
// it is a remount input, and leaving it out of the dependency list
|
|
58
|
+
// meant switching customers kept the previous customer's wallet — and
|
|
59
|
+
// its "set default" / "remove" actions — on screen.
|
|
60
|
+
props: ReactElementProps & { customerId?: string },
|
|
61
|
+
): {
|
|
62
|
+
isClient: boolean;
|
|
63
|
+
containerRef: React.RefObject<HTMLDivElement>;
|
|
64
|
+
handleRef: React.RefObject<BillKitElementHandle | null>;
|
|
65
|
+
} {
|
|
57
66
|
const { iframeOrigin, apiBase, logger: providerLogger } = useBillKit();
|
|
58
67
|
const [isClient, setIsClient] = useState(false);
|
|
59
68
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -101,10 +110,10 @@ export function useElement(
|
|
|
101
110
|
handle.destroy();
|
|
102
111
|
handleRef.current = null;
|
|
103
112
|
};
|
|
104
|
-
// Remount only on identity inputs (secret / origins);
|
|
105
|
-
// read through a ref and theme is hot-applied in the
|
|
106
|
-
// neither belongs in this dependency list.
|
|
107
|
-
}, [isClient, props.clientSecret, props.locale, iframeOrigin, apiBase]);
|
|
113
|
+
// Remount only on identity inputs (secret / customer / origins);
|
|
114
|
+
// callbacks are read through a ref and theme is hot-applied in the
|
|
115
|
+
// effect below, so neither belongs in this dependency list.
|
|
116
|
+
}, [isClient, props.clientSecret, props.customerId, props.locale, iframeOrigin, apiBase]);
|
|
108
117
|
|
|
109
118
|
// Hot-apply theme changes without tearing down the iframe.
|
|
110
119
|
useEffect(() => {
|
|
@@ -113,5 +122,57 @@ export function useElement(
|
|
|
113
122
|
}
|
|
114
123
|
}, [themeKey]);
|
|
115
124
|
|
|
116
|
-
return { isClient, containerRef };
|
|
125
|
+
return { isClient, containerRef, handleRef };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The imperative handle `<CheckoutElement/>` and
|
|
130
|
+
* `<PaymentMethodElement/>` expose through `ref`.
|
|
131
|
+
*
|
|
132
|
+
* Everything else about these components is declarative, but submitting
|
|
133
|
+
* is genuinely an *event*, not a state: a merchant's own pay button
|
|
134
|
+
* lives outside the iframe (that is the point of `onChange.complete`),
|
|
135
|
+
* and it has to be able to say "go" exactly once. A `submit` prop would
|
|
136
|
+
* have to be a toggling boolean, which is the classic React smell for an
|
|
137
|
+
* action modelled as state, so this follows the
|
|
138
|
+
* `useImperativeHandle` path that `<input>`'s `focus()` set.
|
|
139
|
+
*
|
|
140
|
+
* Calls made before the iframe has mounted are no-ops rather than
|
|
141
|
+
* throwing — on the server, and on the first client render, there is no
|
|
142
|
+
* element yet.
|
|
143
|
+
*/
|
|
144
|
+
export interface ThemeableElementRef {
|
|
145
|
+
/** Push new theme tokens in without remounting. */
|
|
146
|
+
updateTheme(theme: BillKitThemeTokens): void;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface BillKitElementRef extends ThemeableElementRef {
|
|
150
|
+
/** Submit the form from your own pay button. */
|
|
151
|
+
submit(): void;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Build the ref `<CheckoutElement/>` exposes. */
|
|
155
|
+
export function checkoutElementRef(
|
|
156
|
+
handleRef: React.RefObject<BillKitElementHandle | null>,
|
|
157
|
+
): BillKitElementRef {
|
|
158
|
+
return {
|
|
159
|
+
submit: () => handleRef.current?.submit(),
|
|
160
|
+
updateTheme: (theme) => handleRef.current?.updateTheme(theme),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Build the ref `<PaymentMethodElement/>` exposes.
|
|
166
|
+
*
|
|
167
|
+
* Deliberately no `submit`: the wallet element drops `billkit:submit` on
|
|
168
|
+
* the floor (there is no form to submit — its actions are per-row "set
|
|
169
|
+
* default" / "remove" buttons inside the iframe). Exposing a method that
|
|
170
|
+
* silently does nothing would be worse than not having one.
|
|
171
|
+
*/
|
|
172
|
+
export function themeableElementRef(
|
|
173
|
+
handleRef: React.RefObject<BillKitElementHandle | null>,
|
|
174
|
+
): ThemeableElementRef {
|
|
175
|
+
return {
|
|
176
|
+
updateTheme: (theme) => handleRef.current?.updateTheme(theme),
|
|
177
|
+
};
|
|
117
178
|
}
|