@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 CHANGED
@@ -16,10 +16,12 @@ package's changelog.
16
16
  First public release.
17
17
 
18
18
  ### Added
19
- - `<BillKitProvider>`: holds the publishable key and shared element
20
- configuration for the tree below it, so individual elements take only the
21
- props that vary. Shaped after `@stripe/react-stripe-js` `<Elements>`, which
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
- `publishableKey` on `<BillKitProvider>` is **deprecated, optional and ignored**. BillKit has no publishable-key concept; the API only mints secret keys, which must never reach a browser. The prop was required in 0.1.0 by analogy with Stripe, but nothing ever read it. Pass nothing. It will be removed in the next major.
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
- () => ({ publishableKey, iframeOrigin, apiBase, logger }),
18
- [publishableKey, iframeOrigin, apiBase, logger]
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 CheckoutElement(props) {
77
- const { isClient, containerRef } = useElement(js.mountCheckoutElement, props);
78
- if (!isClient) return null;
79
- return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: props.className, style: props.style });
75
+ function checkoutElementRef(handleRef) {
76
+ return {
77
+ submit: () => handleRef.current?.submit(),
78
+ updateTheme: (theme) => handleRef.current?.updateTheme(theme)
79
+ };
80
80
  }
81
- function PaymentMethodElement(props) {
82
- const { customerId } = props;
83
- const mount = (target, options) => js.mountPaymentMethodElement(target, { ...options, customerId });
84
- const { isClient, containerRef } = useElement(mount, props);
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;
@@ -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":";;;;;;;AA0CA,IAAM,cAAA,GAAiBA,oBAA0C,IAAI,CAAA;AAkB9D,SAAS,eAAA,CAAgB;AAAA,EAC9B,cAAA;AAAA,EACA,YAAA;AAAA,EACA,OAAA;AAAA,EACA,MAAA;AAAA,EACA;AACF,CAAA,EAAsC;AACpC,EAAA,MAAM,KAAA,GAAQC,aAAA;AAAA,IACZ,OAAO,EAAE,cAAA,EAAgB,YAAA,EAAc,SAAS,MAAA,EAAO,CAAA;AAAA,IACvD,CAAC,cAAA,EAAgB,YAAA,EAAc,OAAA,EAAS,MAAM;AAAA,GAChD;AACA,EAAA,uBAAOC,cAAA,CAAC,cAAA,CAAe,QAAA,EAAf,EAAwB,OAAe,QAAA,EAAS,CAAA;AAC1D;AAOO,SAAS,UAAA,GAAkC;AAChD,EAAA,MAAM,GAAA,GAAMC,iBAAW,cAAc,CAAA;AACrC,EAAA,IAAI,QAAQ,IAAA,EAAM;AAChB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AACA,EAAA,OAAO,GAAA;AACT;ACnCO,SAAS,UAAA,CACd,OACA,KAAA,EACsE;AACtE,EAAA,MAAM,EAAE,YAAA,EAAc,OAAA,EAAS,MAAA,EAAQ,cAAA,KAAmB,UAAA,EAAW;AACrE,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAIC,eAAS,KAAK,CAAA;AAC9C,EAAA,MAAM,YAAA,GAAeC,aAAuB,IAAI,CAAA;AAChD,EAAA,MAAM,SAAA,GAAYA,aAAoC,IAAI,CAAA;AAG1D,EAAA,MAAM,SAAA,GAAYA,aAAO,KAAK,CAAA;AAC9B,EAAA,SAAA,CAAU,OAAA,GAAU,KAAA;AAOpB,EAAA,MAAM,YAAA,GAAeA,aAAyC,MAAS,CAAA;AACvE,EAAA,YAAA,CAAa,OAAA,GAAU,MAAM,MAAA,IAAU,cAAA;AACvC,EAAA,MAAM,eAAeA,YAAA,CAA6B;AAAA,IAChD,KAAA,EAAO,CAAC,OAAA,EAAS,OAAA,KAAY,aAAa,OAAA,EAAS,KAAA,CAAM,SAAS,OAAO,CAAA;AAAA,IACzE,IAAA,EAAM,CAAC,OAAA,EAAS,OAAA,KAAY,aAAa,OAAA,EAAS,IAAA,CAAK,SAAS,OAAO;AAAA,GACxE,CAAA,CAAE,OAAA;AAEH,EAAAC,eAAA,CAAU,MAAM,WAAA,CAAY,IAAI,CAAA,EAAG,EAAE,CAAA;AAErC,EAAA,MAAM,WAAW,KAAA,CAAM,KAAA,GAAQ,KAAK,SAAA,CAAU,KAAA,CAAM,KAAK,CAAA,GAAI,EAAA;AAE7D,EAAAA,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,QAAA,IAAY,YAAA,CAAa,OAAA,KAAY,IAAA,EAAM;AAChD,IAAA,MAAM,OAAA,GAA8B;AAAA,MAClC,cAAc,KAAA,CAAM,YAAA;AAAA,MACpB,GAAI,MAAM,KAAA,GAAQ,EAAE,OAAO,KAAA,CAAM,KAAA,KAAU,EAAC;AAAA,MAC5C,GAAI,MAAM,MAAA,GAAS,EAAE,QAAQ,KAAA,CAAM,MAAA,KAAW,EAAC;AAAA,MAC/C,GAAI,YAAA,GAAe,EAAE,YAAA,KAAiB,EAAC;AAAA,MACvC,GAAI,OAAA,GAAU,EAAE,OAAA,KAAY,EAAC;AAAA,MAC7B,GAAI,MAAM,aAAA,KAAkB,MAAA,GAAY,EAAE,aAAA,EAAe,KAAA,CAAM,aAAA,EAAc,GAAI,EAAC;AAAA,MAClF,MAAA,EAAQ,YAAA;AAAA,MACR,OAAA,EAAS,MAAM,SAAA,CAAU,OAAA,CAAQ,OAAA,IAAU;AAAA,MAC3C,UAAU,CAAC,CAAA,KAAM,SAAA,CAAU,OAAA,CAAQ,WAAW,CAAC,CAAA;AAAA,MAC/C,WAAW,CAAC,CAAA,KAAM,SAAA,CAAU,OAAA,CAAQ,YAAY,CAAC,CAAA;AAAA,MACjD,SAAS,CAAC,CAAA,KAAM,SAAA,CAAU,OAAA,CAAQ,UAAU,CAAC,CAAA;AAAA,MAC7C,YAAY,CAAC,GAAA,KAAQ,SAAA,CAAU,OAAA,CAAQ,aAAa,GAAG;AAAA,KACzD;AACA,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,YAAA,CAAa,OAAA,EAAS,OAAO,CAAA;AAClD,IAAA,SAAA,CAAU,OAAA,GAAU,MAAA;AACpB,IAAA,OAAO,MAAM;AACX,MAAA,MAAA,CAAO,OAAA,EAAQ;AACf,MAAA,SAAA,CAAU,OAAA,GAAU,IAAA;AAAA,IACtB,CAAA;AAAA,EAIF,CAAA,EAAG,CAAC,QAAA,EAAU,KAAA,CAAM,cAAc,KAAA,CAAM,MAAA,EAAQ,YAAA,EAAc,OAAO,CAAC,CAAA;AAGtE,EAAAA,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,QAAA,IAAY,SAAA,CAAU,OAAA,IAAW,KAAA,CAAM,KAAA,EAAO;AAChD,MAAA,SAAA,CAAU,OAAA,CAAQ,WAAA,CAAY,KAAA,CAAM,KAAK,CAAA;AAAA,IAC3C;AAAA,EACF,CAAA,EAAG,CAAC,QAAQ,CAAC,CAAA;AAEb,EAAA,OAAO,EAAE,UAAU,YAAA,EAAa;AAClC;AChGO,SAAS,gBAAgB,KAAA,EAAiD;AAC/E,EAAA,MAAM,EAAE,QAAA,EAAU,YAAA,EAAa,GAAI,UAAA,CAAWC,yBAAsB,KAAK,CAAA;AACzE,EAAA,IAAI,CAAC,UAAU,OAAO,IAAA;AACtB,EAAA,uBAAOL,cAAAA,CAAC,KAAA,EAAA,EAAI,GAAA,EAAK,YAAA,EAAc,WAAW,KAAA,CAAM,SAAA,EAAW,KAAA,EAAO,KAAA,CAAM,KAAA,EAAO,CAAA;AACjF;ACZO,SAAS,qBAAqB,KAAA,EAAsD;AACzF,EAAA,MAAM,EAAE,YAAW,GAAI,KAAA;AACvB,EAAA,MAAM,KAAA,GAAQ,CAAC,MAAA,EAAqB,OAAA,KAClCM,4BAAA,CAA0B,QAAQ,EAAE,GAAG,OAAA,EAAS,UAAA,EAAY,CAAA;AAC9D,EAAA,MAAM,EAAE,QAAA,EAAU,YAAA,EAAa,GAAI,UAAA,CAAW,OAAO,KAAK,CAAA;AAC1D,EAAA,IAAI,CAAC,UAAU,OAAO,IAAA;AACtB,EAAA,uBAAON,cAAAA,CAAC,KAAA,EAAA,EAAI,GAAA,EAAK,YAAA,EAAc,WAAW,KAAA,CAAM,SAAA,EAAW,KAAA,EAAO,KAAA,CAAM,KAAA,EAAO,CAAA;AACjF","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: put your public key
8
- * (and any origin overrides) here once, then render elements freely
9
- * beneath it.
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({ publishableKey, iframeOrigin, apiBase, logger, children, }: BillKitProviderProps): JSX.Element;
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 function CheckoutElement(props: CheckoutElementProps): JSX.Element | null;
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 function PaymentMethodElement(props: PaymentMethodElementProps): JSX.Element | null;
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: put your public key
8
- * (and any origin overrides) here once, then render elements freely
9
- * beneath it.
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({ publishableKey, iframeOrigin, apiBase, logger, children, }: BillKitProviderProps): JSX.Element;
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 function CheckoutElement(props: CheckoutElementProps): JSX.Element | null;
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 function PaymentMethodElement(props: PaymentMethodElementProps): JSX.Element | null;
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, useMemo, useContext, useState, useRef, useEffect } from 'react';
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
- () => ({ publishableKey, iframeOrigin, apiBase, logger }),
16
- [publishableKey, iframeOrigin, apiBase, logger]
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 CheckoutElement(props) {
75
- const { isClient, containerRef } = useElement(mountCheckoutElement, props);
76
- if (!isClient) return null;
77
- return /* @__PURE__ */ jsx("div", { ref: containerRef, className: props.className, style: props.style });
73
+ function checkoutElementRef(handleRef) {
74
+ return {
75
+ submit: () => handleRef.current?.submit(),
76
+ updateTheme: (theme) => handleRef.current?.updateTheme(theme)
77
+ };
78
78
  }
79
- function PaymentMethodElement(props) {
80
- const { customerId } = props;
81
- const mount = (target, options) => mountPaymentMethodElement(target, { ...options, customerId });
82
- const { isClient, containerRef } = useElement(mount, props);
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.1.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": "^0.1.0",
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": "^0.1.0",
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",
@@ -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: put your public key
7
- * (and any origin overrides) here once, then render elements freely
8
- * beneath it.
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
- () => ({ publishableKey, iframeOrigin, apiBase, logger }),
70
- [publishableKey, iframeOrigin, apiBase, logger],
56
+ () => ({ iframeOrigin, apiBase, logger }),
57
+ [iframeOrigin, apiBase, logger],
71
58
  );
72
59
  return <BillKitContext.Provider value={value}>{children}</BillKitContext.Provider>;
73
60
  }
@@ -1,5 +1,11 @@
1
1
  import { mountCheckoutElement } from "@billkit-eu/js";
2
- import { type ReactElementProps, useElement } from "./useElement";
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 function CheckoutElement(props: CheckoutElementProps): JSX.Element | null {
22
- const { isClient, containerRef } = useElement(mountCheckoutElement, props);
23
- if (!isClient) return null;
24
- return <div ref={containerRef} className={props.className} style={props.style} />;
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 { type ReactElementProps, useElement } from "./useElement";
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 function PaymentMethodElement(props: PaymentMethodElementProps): JSX.Element | null {
14
- const { customerId } = props;
15
- const mount = (target: HTMLElement, options: BaseElementOptions): BillKitElementHandle =>
16
- mountPaymentMethodElement(target, { ...options, customerId });
17
- const { isClient, containerRef } = useElement(mount, props);
18
- if (!isClient) return null;
19
- return <div ref={containerRef} className={props.className} style={props.style} />;
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 { ReactElementProps } from "./useElement";
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
- props: ReactElementProps,
56
- ): { isClient: boolean; containerRef: React.RefObject<HTMLDivElement> } {
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); callbacks are
105
- // read through a ref and theme is hot-applied in the effect below, so
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
  }