@n1xv/stripe-elements 0.0.0-stage → 0.1.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/README.md CHANGED
@@ -1,3 +1,104 @@
1
- # Temporary Holding Version
1
+ # @n1xv/stripe-elements
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Stripe Elements on **your** checkout screen, instead of a redirect to someone
4
+ else's.
5
+
6
+ Hosted Checkout is one line and works. What it cannot do is sit inside your own
7
+ page: no wallet row above your paywall button, no consent sentence recorded
8
+ against the press, no saved card for a returning buyer, and no control over the
9
+ screen where the money is actually taken.
10
+
11
+ This package is the mechanics of doing it yourself. It is **not** a checkout —
12
+ there is no modal, no copy, no price and no consent text here. Those belong to
13
+ the product.
14
+
15
+ ## What it does
16
+
17
+ ```ts
18
+ import { mountElements } from '@n1xv/stripe-elements';
19
+
20
+ const checkout = await mountElements(
21
+ // The charge does not exist yet. It is created when Pay is pressed.
22
+ { deferred: { amount: 1200, currency: 'USD', recurring: true } },
23
+ {
24
+ publishableKey: () => fetch('/api/billing/config').then((r) => r.json()).then((c) => c.publishableKey),
25
+ slots: { payment: '#card', express: '#wallets' },
26
+ onChange: (complete) => setCanPay(complete),
27
+ billing: { country }, // collected before this, so Stripe stops asking
28
+ wallet: {
29
+ ready: () => Boolean(total), // a wallet sheet fixes its amount when it opens
30
+ onNotReady: () => toast('One moment — still pricing.'),
31
+ create: () => createChargeOnServer(),
32
+ },
33
+ onWalletResult: (r) => r.outcome === 'completed' && done(),
34
+ },
35
+ );
36
+
37
+ if (!checkout) return; // payments are switched off; render the screen anyway
38
+
39
+ // On your Pay button:
40
+ const problem = await checkout.submit(); // validates before anything is created
41
+ if (problem) return show(problem);
42
+ const secret = await createChargeOnServer(); // now, not before
43
+ const result = await checkout.confirm(secret);
44
+ ```
45
+
46
+ `null` from `mountElements` is an ordinary answer: a fresh clone has no keys and
47
+ must still render.
48
+
49
+ ## Why deferred
50
+
51
+ The charge is created when Pay is pressed, not to make a form appear. Creating
52
+ it first means every buyer who merely *looked* at the card form has left an
53
+ incomplete subscription behind — and the consent has to be ticked before a form
54
+ that does not exist yet.
55
+
56
+ ## What is decided here, and what is not
57
+
58
+ Nothing in this package grants anything or decides a price. Both are settled
59
+ server-side before a secret exists, and entitlements follow the webhook a moment
60
+ after the payment clears — so poll your own billing state rather than trusting
61
+ what the page saw.
62
+
63
+ ## The three outcomes
64
+
65
+ `completed`, `closed`, `unavailable` — never a boolean. A caller that collapses
66
+ them tells somebody whose card was declined that payments are down.
67
+
68
+ `retry: true` means *this charge is spent*: Stripe refuses further confirmations
69
+ against an intent declined too many times, and every attempt after that fails
70
+ for a reason that has nothing to do with the card being typed. Build a new
71
+ charge.
72
+
73
+ Card errors are passed through in Stripe's own words — "insufficient funds" is
74
+ actionable, "that didn't go through" is not. Everything else is ours and is
75
+ listed in `DEFAULT_MESSAGES`; override it to translate.
76
+
77
+ ## Theme
78
+
79
+ ```ts
80
+ appearance: { scope: '.app', darkClass: 'dark', primaryVariable: '--primary' }
81
+ ```
82
+
83
+ Stripe's iframe cannot inherit CSS, so the palette is handed over. Name the
84
+ element your theme actually lives on: reading `<html>` for a class that is set
85
+ somewhere else is true-always or false-always, and the symptom is a black card
86
+ form inside a white page.
87
+
88
+ ## Keeping the server in step
89
+
90
+ A deferred form is told `paymentMethodTypes` because it has no intent to read
91
+ them from. Keep it equal to the server's `payment_method_types`, or the form
92
+ offers a method the intent will refuse — a decline with no cause the buyer can
93
+ act on.
94
+
95
+ > `@stripe/stripe-js` v10 dropped this field from its mode-based typings while
96
+ > keeping it in v9 and in the published docs. Stripe.js still reads it; the
97
+ > package re-declares it rather than dropping the behaviour.
98
+
99
+ ## Origin
100
+
101
+ Lifted from a production checkout in `nrr-tv`, with the product removed: the
102
+ plan ladder, the credit packs, the legal copy and the modal that composed them
103
+ stayed behind. The comments explaining *why* came with the code, because each
104
+ one is a bug somebody already paid for.
@@ -0,0 +1,31 @@
1
+ import type { Appearance } from '@stripe/stripe-js';
2
+ /**
3
+ * Make Stripe's iframe look like the rest of the app.
4
+ *
5
+ * Read off the live theme rather than hard-coded: an app with a light and a
6
+ * dark mode cannot let the element inherit CSS across an iframe boundary, so a
7
+ * fixed palette here is a white form on a black card for half the users.
8
+ *
9
+ * Which element carries the theme is the caller's to say, and getting it wrong
10
+ * is invisible in every test that does not look at the answer. The version this
11
+ * came from asked `<html>` for an attribute nothing in that app ever set, so
12
+ * the condition was true always and every form rendered in Stripe's night
13
+ * theme — including on a light page. Hence `scope`: name the element the theme
14
+ * actually lives on.
15
+ */
16
+ export type ThemeSource = {
17
+ /**
18
+ * The element the theme and the brand variables live on, as a selector.
19
+ * Defaults to `:root`, which is right when nothing scopes them.
20
+ */
21
+ scope?: string;
22
+ /** The class that means dark. Defaults to `dark`. */
23
+ darkClass?: string;
24
+ /** The CSS custom property holding the accent colour. */
25
+ primaryVariable?: string;
26
+ /** Used when the variable is absent — local dev, or a page without the theme. */
27
+ primaryFallback?: string;
28
+ borderRadius?: string;
29
+ };
30
+ export declare function appearanceFromTheme(source?: ThemeSource): Appearance;
31
+ //# sourceMappingURL=appearance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appearance.d.ts","sourceRoot":"","sources":["../src/appearance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iFAAiF;IACjF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,MAAM,GAAE,WAAgB,GAAG,UAAU,CAuBxE"}
@@ -0,0 +1,17 @@
1
+ export function appearanceFromTheme(source = {}) {
2
+ const { scope = ':root', darkClass = 'dark', primaryVariable = '--primary', primaryFallback = '#7c3aed', borderRadius = '12px', } = source;
3
+ // `:root` always matches, but a named scope may not have rendered yet — and
4
+ // falling through to the document element is better than throwing inside a
5
+ // checkout.
6
+ const element = document.querySelector(scope) ?? document.documentElement;
7
+ const css = getComputedStyle(element);
8
+ const dark = element.classList.contains(darkClass);
9
+ return {
10
+ theme: dark ? 'night' : 'stripe',
11
+ variables: {
12
+ colorPrimary: css.getPropertyValue(primaryVariable).trim() || primaryFallback,
13
+ borderRadius,
14
+ },
15
+ };
16
+ }
17
+ //# sourceMappingURL=appearance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appearance.js","sourceRoot":"","sources":["../src/appearance.ts"],"names":[],"mappings":"AA+BA,MAAM,UAAU,mBAAmB,CAAC,SAAsB,EAAE;IAC1D,MAAM,EACJ,KAAK,GAAG,OAAO,EACf,SAAS,GAAG,MAAM,EAClB,eAAe,GAAG,WAAW,EAC7B,eAAe,GAAG,SAAS,EAC3B,YAAY,GAAG,MAAM,GACtB,GAAG,MAAM,CAAC;IAEX,4EAA4E;IAC5E,2EAA2E;IAC3E,YAAY;IACZ,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,QAAQ,CAAC,eAAe,CAAC;IAC1E,MAAM,GAAG,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAEnD,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ;QAChC,SAAS,EAAE;YACT,YAAY,EAAE,GAAG,CAAC,gBAAgB,CAAC,eAAe,CAAC,CAAC,IAAI,EAAE,IAAI,eAAe;YAC7E,YAAY;SACb;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,146 @@
1
+ import type { Appearance } from '@stripe/stripe-js';
2
+ import { type ThemeSource } from './appearance.js';
3
+ import { type OutcomeMessages } from './outcome.js';
4
+ import { type PublishableKeySource } from './stripe-js.js';
5
+ import type { BillingAddress, ConfirmResult, MountIntent, MountedCheckout, WalletHooks } from './types.js';
6
+ /**
7
+ * Stripe Elements, mounted into your own checkout screen.
8
+ *
9
+ * Vanilla `@stripe/stripe-js` rather than the React bindings: the payments
10
+ * layer stays a plain async API the UI calls, which keeps this package usable
11
+ * from any framework and avoids a second React tree inside a modal.
12
+ *
13
+ * Three elements, and each earns its place:
14
+ *
15
+ * - **Express Checkout** is Apple Pay / Google Pay / Link. It only renders the
16
+ * wallets the visitor can actually use, so on a desktop without any it draws
17
+ * nothing — the caller has to treat an empty express row as normal rather
18
+ * than as a failure to load.
19
+ * - **Payment** is the card form.
20
+ * - **Address** is collected by the caller BEFORE this, not here: the total
21
+ * depends on it and a finalised invoice cannot be re-taxed.
22
+ *
23
+ * Nothing here grants anything, and nothing here decides a price. Both are
24
+ * settled server-side before a secret exists.
25
+ */
26
+ /**
27
+ * The height of a wallet button, in px.
28
+ *
29
+ * Stripe draws its buttons inside an iframe, so CSS on the host side cannot
30
+ * touch them — the only lever is this option, and its range is 40–55 (default
31
+ * 44). 55 is the top of that range and the closest it can come to an ordinary
32
+ * button, which is why a host's own buttons should be pinned to the same
33
+ * number rather than left at whatever their padding produced.
34
+ */
35
+ export declare const EXPRESS_BUTTON_HEIGHT = 55;
36
+ /**
37
+ * The payment methods a DEFERRED form offers.
38
+ *
39
+ * The same list the intent will be created with, named here because a deferred
40
+ * form has no intent to read it from. Left to the dashboard's automatic
41
+ * methods, the form offers whatever is enabled there and then fails at
42
+ * confirmation against an intent that accepts something narrower — a decline
43
+ * with no cause the buyer can act on. Keep this and the server's
44
+ * `payment_method_types` in step.
45
+ */
46
+ export declare const DEFAULT_PAYMENT_METHOD_TYPES: readonly ["card", "link"];
47
+ export type MountOptions = {
48
+ /** Where each element goes, as selectors. */
49
+ slots: {
50
+ payment?: string;
51
+ express?: string;
52
+ };
53
+ /** Needed before anything can be drawn. */
54
+ publishableKey: PublishableKeySource;
55
+ /** Reports completeness so the caller can enable its own Pay button. */
56
+ onChange?: (complete: boolean) => void;
57
+ /**
58
+ * Lets the form show cards this buyer has already saved.
59
+ *
60
+ * A client secret describes one PAYMENT, not one person, so without this the
61
+ * Payment Element has no way to know whose cards to offer and shows a blank
62
+ * form to everybody — including a subscriber whose card is charged monthly.
63
+ * Absent when Stripe could not mint one, which costs the convenience and
64
+ * nothing else.
65
+ */
66
+ customerSessionSecret?: string;
67
+ /**
68
+ * How a WALLET payment ended.
69
+ *
70
+ * Apple Pay and Google Pay confirm from inside the element's own sheet, not
71
+ * from the caller's Pay button, so their outcome arrives here rather than as
72
+ * the return value of `confirm()`. Without it a successful wallet payment
73
+ * leaves the checkout sitting open on a charge that has already cleared.
74
+ */
75
+ onWalletResult?: (result: ConfirmResult) => void;
76
+ wallet?: WalletHooks;
77
+ /**
78
+ * The buyer's email, so Link can recognise them.
79
+ *
80
+ * Link keeps a wallet against an EMAIL, including one created on somebody
81
+ * else's site. Told it up front, Link offers the saved cards behind it —
82
+ * verifying with a code — instead of an empty form.
83
+ */
84
+ email?: string;
85
+ /**
86
+ * The billing address the caller already collected, when it did.
87
+ *
88
+ * Present it and Stripe stops asking: the Payment Element collects a billing
89
+ * address by default, which puts a SECOND country dropdown under the card on
90
+ * a screen that already asked. Two selectors is not only an extra step — they
91
+ * can disagree, and the tax on the invoice was computed from the caller's.
92
+ */
93
+ billing?: BillingAddress;
94
+ /** Defaults to reading the live theme; pass an object to fix it. */
95
+ appearance?: Appearance | ThemeSource;
96
+ /** Defaults to `['card', 'link']`. Must match the server's intent. */
97
+ paymentMethodTypes?: readonly string[];
98
+ /** Override to translate. See `OutcomeMessages`. */
99
+ messages?: OutcomeMessages;
100
+ /**
101
+ * Where a redirecting method brings the buyer back. Defaults to the current
102
+ * origin and path.
103
+ *
104
+ * The query is dropped deliberately: a buyer coming back from a redirect
105
+ * carries Stripe's own result parameters in it, and reusing that as the next
106
+ * return address sends them back to a stale secret.
107
+ */
108
+ returnUrl?: () => string;
109
+ };
110
+ /**
111
+ * Mount the payment form — for a server-created secret, or ahead of one.
112
+ *
113
+ * Returns null when Stripe.js could not load or payments are switched off,
114
+ * which is an ordinary answer rather than a throw: a fresh clone with no keys
115
+ * must still render its screen.
116
+ */
117
+ export declare function mountElements(intent: MountIntent, options: MountOptions): Promise<MountedCheckout | null>;
118
+ /**
119
+ * Mount a form that stores a card without charging it.
120
+ *
121
+ * No wallet row: Apple Pay and Google Pay can be saved, but the reason to be on
122
+ * this screen is to put a card on file for renewals, and a wallet tile there
123
+ * reads as a way to pay for something.
124
+ */
125
+ export declare function mountCardSetup(clientSecret: string, options: Omit<MountOptions, 'slots' | 'wallet' | 'onWalletResult'> & {
126
+ slot: string;
127
+ }): Promise<MountedCheckout | null>;
128
+ /**
129
+ * Answer a challenge on a payment that already exists.
130
+ *
131
+ * Nothing is collected and nothing is mounted: the card is the one on file, the
132
+ * charge was raised server-side, and the only thing missing is the cardholder
133
+ * tapping their bank's prompt.
134
+ *
135
+ * `retry` keeps its meaning — this payment is spent. What a fresh one LOOKS
136
+ * like differs: a checkout screen rebuilds its own charge, while here the
137
+ * charge belongs to an update the provider is holding, so the only way forward
138
+ * is a working card on file and a fresh amendment. Stripping the flag here
139
+ * leaves the caller with `unavailable` and no way to tell "the card on file was
140
+ * refused for good" from "Stripe.js did not load".
141
+ */
142
+ export declare function authenticate(clientSecret: string, options: {
143
+ publishableKey: PublishableKeySource;
144
+ messages?: OutcomeMessages;
145
+ }): Promise<ConfirmResult>;
146
+ //# sourceMappingURL=elements.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elements.d.ts","sourceRoot":"","sources":["../src/elements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EAKX,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAA0C,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAC5F,OAAO,EAAgB,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACzE,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EAEb,WAAW,EACX,eAAe,EACf,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;;;;;;;GASG;AACH,eAAO,MAAM,4BAA4B,2BAA4B,CAAC;AAatE,MAAM,MAAM,YAAY,GAAG;IACzB,6CAA6C;IAC7C,KAAK,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,2CAA2C;IAC3C,cAAc,EAAE,oBAAoB,CAAC;IACrC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC;;;;;;;;OAQG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IACjD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,oEAAoE;IACpE,UAAU,CAAC,EAAE,UAAU,GAAG,WAAW,CAAC;IACtC,sEAAsE;IACtE,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,oDAAoD;IACpD,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,MAAM,CAAC;CAC1B,CAAC;AAOF;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CA6OjC;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,OAAO,GAAG,QAAQ,GAAG,gBAAgB,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GACpF,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAGjC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CAChC,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE;IAAE,cAAc,EAAE,oBAAoB,CAAC;IAAC,QAAQ,CAAC,EAAE,eAAe,CAAA;CAAE,GAC5E,OAAO,CAAC,aAAa,CAAC,CAMxB"}
@@ -0,0 +1,375 @@
1
+ import { appearanceFromTheme } from './appearance.js';
2
+ import { classifyConfirmError, DEFAULT_MESSAGES } from './outcome.js';
3
+ import { loadStripeJs } from './stripe-js.js';
4
+ /**
5
+ * Stripe Elements, mounted into your own checkout screen.
6
+ *
7
+ * Vanilla `@stripe/stripe-js` rather than the React bindings: the payments
8
+ * layer stays a plain async API the UI calls, which keeps this package usable
9
+ * from any framework and avoids a second React tree inside a modal.
10
+ *
11
+ * Three elements, and each earns its place:
12
+ *
13
+ * - **Express Checkout** is Apple Pay / Google Pay / Link. It only renders the
14
+ * wallets the visitor can actually use, so on a desktop without any it draws
15
+ * nothing — the caller has to treat an empty express row as normal rather
16
+ * than as a failure to load.
17
+ * - **Payment** is the card form.
18
+ * - **Address** is collected by the caller BEFORE this, not here: the total
19
+ * depends on it and a finalised invoice cannot be re-taxed.
20
+ *
21
+ * Nothing here grants anything, and nothing here decides a price. Both are
22
+ * settled server-side before a secret exists.
23
+ */
24
+ /**
25
+ * The height of a wallet button, in px.
26
+ *
27
+ * Stripe draws its buttons inside an iframe, so CSS on the host side cannot
28
+ * touch them — the only lever is this option, and its range is 40–55 (default
29
+ * 44). 55 is the top of that range and the closest it can come to an ordinary
30
+ * button, which is why a host's own buttons should be pinned to the same
31
+ * number rather than left at whatever their padding produced.
32
+ */
33
+ export const EXPRESS_BUTTON_HEIGHT = 55;
34
+ /**
35
+ * The payment methods a DEFERRED form offers.
36
+ *
37
+ * The same list the intent will be created with, named here because a deferred
38
+ * form has no intent to read it from. Left to the dashboard's automatic
39
+ * methods, the form offers whatever is enabled there and then fails at
40
+ * confirmation against an intent that accepts something narrower — a decline
41
+ * with no cause the buyer can act on. Keep this and the server's
42
+ * `payment_method_types` in step.
43
+ */
44
+ export const DEFAULT_PAYMENT_METHOD_TYPES = ['card', 'link'];
45
+ const isThemeSource = (a) => a !== undefined && !('theme' in a) && !('variables' in a) && !('rules' in a);
46
+ const defaultReturnUrl = () => `${window.location.origin}${window.location.pathname}`;
47
+ /**
48
+ * Mount the payment form — for a server-created secret, or ahead of one.
49
+ *
50
+ * Returns null when Stripe.js could not load or payments are switched off,
51
+ * which is an ordinary answer rather than a throw: a fresh clone with no keys
52
+ * must still render its screen.
53
+ */
54
+ export async function mountElements(intent, options) {
55
+ const { slots, publishableKey, onChange, customerSessionSecret, onWalletResult, wallet, email, billing, paymentMethodTypes = DEFAULT_PAYMENT_METHOD_TYPES, messages = DEFAULT_MESSAGES, returnUrl = defaultReturnUrl, } = options;
56
+ const stripe = await loadStripeJs(publishableKey).catch(() => null);
57
+ if (!stripe)
58
+ return null;
59
+ const appearance = isThemeSource(options.appearance)
60
+ ? appearanceFromTheme(options.appearance)
61
+ : (options.appearance ?? appearanceFromTheme());
62
+ const deferred = 'deferred' in intent ? intent.deferred : null;
63
+ const kind = 'deferred' in intent ? 'payment' : (intent.kind ?? 'payment');
64
+ let complete = false;
65
+ // Everything that does not depend on which kind of form this is. Stripe types
66
+ // the two shapes as separate overloads, so the CALL has to branch even though
67
+ // most of the options do not.
68
+ const shared = {
69
+ appearance,
70
+ ...(customerSessionSecret ? { customerSessionClientSecret: customerSessionSecret } : {}),
71
+ };
72
+ /**
73
+ * The two modes are spelled out rather than computed, because Stripe types
74
+ * them as separate members of a union: a `mode` narrowed to
75
+ * `'subscription' | 'payment'` matches neither, and the call stops
76
+ * typechecking for a reason that has nothing to do with it being wrong.
77
+ *
78
+ * Something that renews unattended needs a card usable off-session; a
79
+ * one-off is over when it is paid, and keeping the card is the buyer's own
80
+ * tick in the element.
81
+ */
82
+ const deferredOptions = deferred
83
+ ? deferred.recurring
84
+ ? {
85
+ mode: 'subscription',
86
+ amount: deferred.amount,
87
+ currency: deferred.currency.toLowerCase(),
88
+ setupFutureUsage: 'off_session',
89
+ paymentMethodTypes: [...paymentMethodTypes],
90
+ ...shared,
91
+ }
92
+ : {
93
+ mode: 'payment',
94
+ amount: deferred.amount,
95
+ currency: deferred.currency.toLowerCase(),
96
+ paymentMethodTypes: [...paymentMethodTypes],
97
+ ...shared,
98
+ }
99
+ : null;
100
+ const elements = deferredOptions
101
+ ? stripe.elements(deferredOptions)
102
+ : stripe.elements({ clientSecret: intent.secret, ...shared });
103
+ // A country we do not have cannot be sent, and suppressing the field without
104
+ // sending one is a dead end: Stripe refuses the confirmation and the buyer
105
+ // has nowhere to type the answer. Falling back to Stripe's own field costs a
106
+ // duplicate dropdown in a case that should not happen — the cheaper failure.
107
+ const address = billing?.country ? billing : undefined;
108
+ const payment = slots.payment
109
+ ? elements.create('payment', {
110
+ layout: 'tabs',
111
+ ...(email ? { defaultValues: { billingDetails: { email } } } : {}),
112
+ // Suppress EXACTLY what was already asked for, field by field.
113
+ // `address: "never"` hides all of it and then Stripe demands a complete
114
+ // address back at confirmation; naming the fields keeps the contract to
115
+ // the ones the caller can satisfy.
116
+ ...(address
117
+ ? {
118
+ fields: {
119
+ billingDetails: {
120
+ address: {
121
+ country: 'never',
122
+ postalCode: address.postalCode ? 'never' : 'auto',
123
+ line1: 'auto',
124
+ line2: 'auto',
125
+ city: 'auto',
126
+ state: 'auto',
127
+ },
128
+ },
129
+ },
130
+ }
131
+ : {}),
132
+ })
133
+ : null;
134
+ if (payment && slots.payment) {
135
+ payment.on('change', (e) => {
136
+ complete = e.complete;
137
+ onChange?.(complete);
138
+ });
139
+ payment.mount(slots.payment);
140
+ }
141
+ const submit = async () => {
142
+ const { error } = await elements.submit();
143
+ // Stripe's own words: "Your card number is incomplete" tells the buyer
144
+ // where to look, and a sentence of ours would not.
145
+ return error ? (error.message ?? messages.declined) : null;
146
+ };
147
+ /**
148
+ * The wallet row, and the handler without which it is decoration.
149
+ *
150
+ * A wallet button does NOT pay by itself: pressing it opens the sheet and
151
+ * then waits for a `confirm` handler to take it from there. Mounted without
152
+ * one, Apple Pay and Google Pay open, collect a card, and go nowhere — the
153
+ * worst version of a payment method that does not work, because the buyer has
154
+ * every reason to believe they have just paid.
155
+ */
156
+ let express = null;
157
+ if (slots.express && document.querySelector(slots.express)) {
158
+ try {
159
+ const el = elements.create('expressCheckout', {
160
+ /**
161
+ * Wallets first, Link last.
162
+ *
163
+ * Stripe's default is a dynamic order it optimises per visitor, which
164
+ * puts Link — a Stripe account, i.e. one more thing to sign into —
165
+ * ahead of the wallet already unlocked on the device.
166
+ */
167
+ paymentMethodOrder: ['applePay', 'googlePay', 'link'],
168
+ buttonHeight: EXPRESS_BUTTON_HEIGHT,
169
+ layout: {
170
+ /**
171
+ * One per row. Left to Stripe, two wallets share a row and come out
172
+ * half the width of the card button beneath them — three buttons
173
+ * doing the same job in two different sizes, which on a phone reads
174
+ * as two of them being secondary.
175
+ */
176
+ maxColumns: 1,
177
+ /**
178
+ * Never fold the rest behind a "more" button: the overflow menu hides
179
+ * the cheapest conversion there is — a wallet the visitor has already
180
+ * authenticated — behind a second tap.
181
+ */
182
+ overflow: 'never',
183
+ },
184
+ });
185
+ el.on('ready', (event) => {
186
+ const available = event
187
+ .availablePaymentMethods;
188
+ wallet?.onReady?.(Object.keys(available ?? {}).filter((k) => available?.[k]));
189
+ });
190
+ if (wallet) {
191
+ // Refusing to resolve is how Stripe is told to keep the sheet shut —
192
+ // far better than a sheet that opens, takes a fingerprint and then
193
+ // fails on a total that moved under it.
194
+ el.on('click', (event) => {
195
+ if (!wallet.ready())
196
+ return wallet.onNotReady();
197
+ event.resolve();
198
+ });
199
+ }
200
+ el.on('confirm', () => {
201
+ void (async () => {
202
+ const failed = await submit();
203
+ if (failed)
204
+ return onWalletResult?.({ outcome: 'closed', error: failed });
205
+ /**
206
+ * The charge, created only now — the wallet sheet is the one path
207
+ * that reaches a confirmation without passing the caller's Pay
208
+ * button, so it has to raise the charge itself. A null means the
209
+ * caller already knows why, so there is nothing to report twice.
210
+ */
211
+ const secret = deferred ? await wallet?.create() : undefined;
212
+ if (deferred && !secret)
213
+ return;
214
+ // Deliberately WITHOUT the collected address: a wallet supplies its
215
+ // own billing details from the card in it, and `payment_method_data`
216
+ // on that confirmation fights the payment method the sheet produced.
217
+ const result = await confirmPayment(stripe, elements, {
218
+ clientSecret: secret ?? undefined,
219
+ messages,
220
+ returnUrl,
221
+ });
222
+ onWalletResult?.(result);
223
+ })();
224
+ });
225
+ el.mount(slots.express);
226
+ express = el;
227
+ }
228
+ catch {
229
+ /* no wallet available — the card form is still there */
230
+ }
231
+ }
232
+ return {
233
+ complete: () => complete,
234
+ submit,
235
+ /**
236
+ * Re-price without rebuilding.
237
+ *
238
+ * The total moves while the form is open — a buyer who changes their
239
+ * country changes the tax — and a deferred form carries the amount itself,
240
+ * so an element left at the old one confirms an intent whose total no
241
+ * longer matches and Stripe refuses it. Remounting instead would throw away
242
+ * a half-typed card on a country change.
243
+ */
244
+ update: async (charge) => {
245
+ if (!deferred)
246
+ return;
247
+ await elements.update({
248
+ amount: charge.amount,
249
+ currency: charge.currency.toLowerCase(),
250
+ });
251
+ },
252
+ /**
253
+ * BOTH elements, not just the form.
254
+ *
255
+ * Destroying only the payment element leaves the wallet row mounted against
256
+ * a client secret that no longer exists — a Google Pay button sitting under
257
+ * an error message, on a payment that was already abandoned.
258
+ */
259
+ destroy: () => {
260
+ elements.getElement('payment')?.destroy();
261
+ express?.destroy();
262
+ },
263
+ confirm: (clientSecret) => kind === 'setup'
264
+ ? confirmSetup(stripe, elements, { messages, returnUrl })
265
+ : confirmPayment(stripe, elements, { billing: address, clientSecret, messages, returnUrl }),
266
+ };
267
+ }
268
+ /**
269
+ * Mount a form that stores a card without charging it.
270
+ *
271
+ * No wallet row: Apple Pay and Google Pay can be saved, but the reason to be on
272
+ * this screen is to put a card on file for renewals, and a wallet tile there
273
+ * reads as a way to pay for something.
274
+ */
275
+ export function mountCardSetup(clientSecret, options) {
276
+ const { slot, ...rest } = options;
277
+ return mountElements({ secret: clientSecret, kind: 'setup' }, { ...rest, slots: { payment: slot } });
278
+ }
279
+ /**
280
+ * Answer a challenge on a payment that already exists.
281
+ *
282
+ * Nothing is collected and nothing is mounted: the card is the one on file, the
283
+ * charge was raised server-side, and the only thing missing is the cardholder
284
+ * tapping their bank's prompt.
285
+ *
286
+ * `retry` keeps its meaning — this payment is spent. What a fresh one LOOKS
287
+ * like differs: a checkout screen rebuilds its own charge, while here the
288
+ * charge belongs to an update the provider is holding, so the only way forward
289
+ * is a working card on file and a fresh amendment. Stripping the flag here
290
+ * leaves the caller with `unavailable` and no way to tell "the card on file was
291
+ * refused for good" from "Stripe.js did not load".
292
+ */
293
+ export async function authenticate(clientSecret, options) {
294
+ const messages = options.messages ?? DEFAULT_MESSAGES;
295
+ const stripe = await loadStripeJs(options.publishableKey).catch(() => null);
296
+ if (!stripe)
297
+ return { outcome: 'unavailable', error: messages.unavailable };
298
+ const { error } = await stripe.handleNextAction({ clientSecret });
299
+ return classifyConfirmError(error ?? null, messages);
300
+ }
301
+ /**
302
+ * Store the card, and mark it usable everywhere.
303
+ *
304
+ * `allow_redisplay: 'always'` is not a detail — it is the difference between a
305
+ * card that appears in settings and the next checkout, and one that silently
306
+ * does not. A SetupIntent has no field for it and a card confirmed without one
307
+ * comes back `unspecified`, which every "saved cards" surface filters out. The
308
+ * consent behind it is the act itself: this call only happens because somebody
309
+ * opened "Add a card" and pressed save.
310
+ */
311
+ async function confirmSetup(stripe, elements, opts) {
312
+ const { error, setupIntent } = await stripe.confirmSetup({
313
+ elements,
314
+ redirect: 'if_required',
315
+ confirmParams: {
316
+ payment_method_data: { allow_redisplay: 'always' },
317
+ // A setup that pins `card` cannot redirect today — but the obligation
318
+ // belongs to the call, not to the current list, and the day Link joins it
319
+ // this is what stops a hosted-page failure.
320
+ return_url: opts.returnUrl(),
321
+ },
322
+ });
323
+ const result = classifyConfirmError(error ?? null, opts.messages);
324
+ if (result.outcome !== 'completed')
325
+ return result;
326
+ // Which card was stored, so a caller that needs it billed can say so.
327
+ const cardId = typeof setupIntent?.payment_method === 'string'
328
+ ? setupIntent.payment_method
329
+ : setupIntent?.payment_method?.id;
330
+ return cardId ? { ...result, cardId } : result;
331
+ }
332
+ /**
333
+ * Take the payment without leaving the page.
334
+ *
335
+ * `redirect: 'if_required'` keeps the buyer here for cards and only navigates
336
+ * for methods that genuinely cannot complete inline. A plain `redirect:
337
+ * 'always'` would send everyone away and bring them back on a return_url — the
338
+ * same hand-off an own-screen checkout exists to avoid.
339
+ */
340
+ async function confirmPayment(stripe, elements, opts) {
341
+ const { error } = await stripe.confirmPayment({
342
+ elements,
343
+ ...(opts.clientSecret ? { clientSecret: opts.clientSecret } : {}),
344
+ redirect: 'if_required',
345
+ confirmParams: {
346
+ /**
347
+ * REQUIRED, even though almost nobody is sent anywhere.
348
+ *
349
+ * `redirect: 'if_required'` decides whether the buyer travels; it does
350
+ * not excuse the caller from saying where they would land. Stripe refuses
351
+ * the whole confirmation for any method that MIGHT redirect when this is
352
+ * absent — which is how Link comes to open a hosted page reading
353
+ * "Something went wrong".
354
+ */
355
+ return_url: opts.returnUrl(),
356
+ // The address the element was told not to ask for. Only sent when it was
357
+ // suppressed — passing it while the element still collects one would
358
+ // overwrite whatever the buyer typed there.
359
+ ...(opts.billing
360
+ ? {
361
+ payment_method_data: {
362
+ billing_details: {
363
+ address: {
364
+ country: opts.billing.country,
365
+ ...(opts.billing.postalCode ? { postal_code: opts.billing.postalCode } : {}),
366
+ },
367
+ },
368
+ },
369
+ }
370
+ : {}),
371
+ },
372
+ });
373
+ return classifyConfirmError(error ?? null, opts.messages);
374
+ }
375
+ //# sourceMappingURL=elements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elements.js","sourceRoot":"","sources":["../src/elements.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Stripe Elements on your own checkout screen.
3
+ *
4
+ * What this is NOT: a checkout. There is no modal, no copy, no consent
5
+ * sentence and no price here — those belong to the product, and the version
6
+ * this was taken from proved it by being 818 lines of them. What is shared
7
+ * between products is the mechanics underneath: mounting the three elements,
8
+ * confirming against a charge that may not exist yet, and agreeing on what a
9
+ * failure means.
10
+ *
11
+ * Nothing in this package grants anything. Entitlements follow the webhook, a
12
+ * moment after the payment clears — so a caller polls its own billing state
13
+ * rather than trusting what the page saw.
14
+ */
15
+ export { mountElements, mountCardSetup, authenticate, EXPRESS_BUTTON_HEIGHT, DEFAULT_PAYMENT_METHOD_TYPES } from './elements.js';
16
+ export type { MountOptions } from './elements.js';
17
+ export { loadStripeJs, resetStripeJs } from './stripe-js.js';
18
+ export type { PublishableKeySource } from './stripe-js.js';
19
+ export { appearanceFromTheme } from './appearance.js';
20
+ export type { ThemeSource } from './appearance.js';
21
+ export { classifyConfirmError, DEFAULT_MESSAGES } from './outcome.js';
22
+ export type { OutcomeMessages } from './outcome.js';
23
+ export type { BillingAddress, CheckoutOutcome, ConfirmResult, DeferredCharge, MountIntent, MountedCheckout, WalletHooks, } from './types.js';
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,qBAAqB,EAAE,4BAA4B,EAAE,MAAM,eAAe,CAAC;AACjI,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC7D,YAAY,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACtE,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,YAAY,EACV,cAAc,EACd,eAAe,EACf,aAAa,EACb,cAAc,EACd,WAAW,EACX,eAAe,EACf,WAAW,GACZ,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Stripe Elements on your own checkout screen.
3
+ *
4
+ * What this is NOT: a checkout. There is no modal, no copy, no consent
5
+ * sentence and no price here — those belong to the product, and the version
6
+ * this was taken from proved it by being 818 lines of them. What is shared
7
+ * between products is the mechanics underneath: mounting the three elements,
8
+ * confirming against a charge that may not exist yet, and agreeing on what a
9
+ * failure means.
10
+ *
11
+ * Nothing in this package grants anything. Entitlements follow the webhook, a
12
+ * moment after the payment clears — so a caller polls its own billing state
13
+ * rather than trusting what the page saw.
14
+ */
15
+ export { mountElements, mountCardSetup, authenticate, EXPRESS_BUTTON_HEIGHT, DEFAULT_PAYMENT_METHOD_TYPES } from './elements.js';
16
+ export { loadStripeJs, resetStripeJs } from './stripe-js.js';
17
+ export { appearanceFromTheme } from './appearance.js';
18
+ export { classifyConfirmError, DEFAULT_MESSAGES } from './outcome.js';
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,qBAAqB,EAAE,4BAA4B,EAAE,MAAM,eAAe,CAAC;AAGjI,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAGtD,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,39 @@
1
+ import type { StripeError } from '@stripe/stripe-js';
2
+ import type { ConfirmResult } from './types.js';
3
+ /**
4
+ * What a Stripe failure means for the buyer, in one place.
5
+ *
6
+ * Shared by the card confirmation, the wallet sheet and the 3D Secure challenge
7
+ * — three entrances to the same set of outcomes. Everything that is not a card
8
+ * error used to collapse into one sentence with no way out but reloading the
9
+ * page, which is how a checkout comes to reject cards that worked minutes
10
+ * earlier.
11
+ *
12
+ * Exported, and pure, on purpose: the version this was taken from lived inside
13
+ * the module that loads Stripe.js, so its own test restated the rules in prose
14
+ * instead of calling them. A rule stated twice is a rule that drifts.
15
+ */
16
+ /**
17
+ * The sentences shown when the failure is ours to explain.
18
+ *
19
+ * A card error is NOT in here: Stripe's own wording is better than anything
20
+ * written in advance — "insufficient funds" is actionable, "that didn't go
21
+ * through" is not — so it is passed through untouched.
22
+ *
23
+ * Override them to translate. The keys are the failure, not the copy.
24
+ */
25
+ export type OutcomeMessages = {
26
+ /** A card error with no message of its own. */
27
+ declined: string;
28
+ /** Too many attempts in too short a time. */
29
+ rateLimited: string;
30
+ /** The request never arrived. */
31
+ offline: string;
32
+ /** This charge is spent; a fresh one is the only way forward. */
33
+ expired: string;
34
+ /** Ours to fix — a bad key, an outage. */
35
+ unavailable: string;
36
+ };
37
+ export declare const DEFAULT_MESSAGES: OutcomeMessages;
38
+ export declare function classifyConfirmError(error: StripeError | null, messages?: OutcomeMessages): ConfirmResult;
39
+ //# sourceMappingURL=outcome.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outcome.d.ts","sourceRoot":"","sources":["../src/outcome.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;;;;;;;;;GAYG;AAEH;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,WAAW,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,eAM9B,CAAC;AAEF,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,WAAW,GAAG,IAAI,EACzB,QAAQ,GAAE,eAAkC,GAC3C,aAAa,CA6Cf"}
@@ -0,0 +1,50 @@
1
+ export const DEFAULT_MESSAGES = {
2
+ declined: 'That payment was declined.',
3
+ rateLimited: 'Too many attempts just now. Wait a moment and try again.',
4
+ offline: 'Connection lost. Try again.',
5
+ expired: 'This payment expired. Press pay again to start a new one.',
6
+ unavailable: 'Payments are unavailable right now.',
7
+ };
8
+ export function classifyConfirmError(error, messages = DEFAULT_MESSAGES) {
9
+ if (!error)
10
+ return { outcome: 'completed' };
11
+ // The buyer's problem to fix, and Stripe's own words are better than ours.
12
+ if (error.type === 'card_error' || error.type === 'validation_error') {
13
+ return { outcome: 'closed', error: error.message ?? messages.declined };
14
+ }
15
+ // Retrying immediately makes it worse, and recreating the charge does not
16
+ // help — the limit is on the account, not on this payment.
17
+ if (error.type === 'rate_limit_error') {
18
+ return { outcome: 'closed', error: messages.rateLimited };
19
+ }
20
+ /**
21
+ * The request never arrived. The payment is untouched, so the ONLY right move
22
+ * is to send the same one again — rebuilding it would abandon a perfectly
23
+ * good charge and tell the buyer their payment expired because their wifi
24
+ * blinked.
25
+ */
26
+ if (error.type === 'api_connection_error') {
27
+ return { outcome: 'closed', error: messages.offline };
28
+ }
29
+ /**
30
+ * The payment itself is no longer usable.
31
+ *
32
+ * Reached after a run of failures: Stripe stops accepting confirmations
33
+ * against an intent that has been declined repeatedly, and every further
34
+ * attempt is refused for a reason that has nothing to do with the card now
35
+ * being typed. Without separating this out it shows as one opaque sentence on
36
+ * every card — including cards that worked minutes earlier — and the only way
37
+ * out is reloading the page, which quietly starts a fresh charge. `retry`
38
+ * says so, and the caller starts that fresh charge itself.
39
+ */
40
+ if (error.type === 'invalid_request_error') {
41
+ return { outcome: 'unavailable', retry: true, error: messages.expired };
42
+ }
43
+ /**
44
+ * Ours, not theirs. Deliberately NOT a rebuild: a new charge against a broken
45
+ * configuration is dead on arrival, and inviting the buyer to press again
46
+ * turns one failure into a row of them.
47
+ */
48
+ return { outcome: 'unavailable', error: messages.unavailable };
49
+ }
50
+ //# sourceMappingURL=outcome.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outcome.js","sourceRoot":"","sources":["../src/outcome.ts"],"names":[],"mappings":"AAwCA,MAAM,CAAC,MAAM,gBAAgB,GAAoB;IAC/C,QAAQ,EAAE,4BAA4B;IACtC,WAAW,EAAE,0DAA0D;IACvE,OAAO,EAAE,6BAA6B;IACtC,OAAO,EAAE,2DAA2D;IACpE,WAAW,EAAE,qCAAqC;CACnD,CAAC;AAEF,MAAM,UAAU,oBAAoB,CAClC,KAAyB,EACzB,WAA4B,gBAAgB;IAE5C,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;IAE5C,2EAA2E;IAC3E,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,IAAI,KAAK,CAAC,IAAI,KAAK,kBAAkB,EAAE,CAAC;QACrE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;IAC1E,CAAC;IAED,0EAA0E;IAC1E,2DAA2D;IAC3D,IAAI,KAAK,CAAC,IAAI,KAAK,kBAAkB,EAAE,CAAC;QACtC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,CAAC,WAAW,EAAE,CAAC;IAC5D,CAAC;IAED;;;;;OAKG;IACH,IAAI,KAAK,CAAC,IAAI,KAAK,sBAAsB,EAAE,CAAC;QAC1C,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC;IACxD,CAAC;IAED;;;;;;;;;;OAUG;IACH,IAAI,KAAK,CAAC,IAAI,KAAK,uBAAuB,EAAE,CAAC;QAC3C,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC;IAC1E,CAAC;IAED;;;;OAIG;IACH,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,QAAQ,CAAC,WAAW,EAAE,CAAC;AACjE,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { type Stripe } from '@stripe/stripe-js';
2
+ /**
3
+ * Loading Stripe.js, once per page.
4
+ *
5
+ * The key is a function rather than a string because a template fetches it from
6
+ * its own server (`/api/billing/config`) and does not have it at import time.
7
+ * Returning an empty string is the "payments are switched off" answer, and it
8
+ * is an ordinary one: a fresh clone has no keys and must still render.
9
+ *
10
+ * Only a successful load is memoised. A flaky network on the first attempt must
11
+ * not leave the page permanently convinced that payments are off.
12
+ */
13
+ export type PublishableKeySource = string | (() => Promise<string | null> | string | null);
14
+ export declare function loadStripeJs(key: PublishableKeySource): Promise<Stripe | null>;
15
+ /**
16
+ * Forget the memoised load.
17
+ *
18
+ * For tests, and for an app that can switch accounts without a reload. Nothing
19
+ * in a normal page lifetime needs it.
20
+ */
21
+ export declare function resetStripeJs(): void;
22
+ //# sourceMappingURL=stripe-js.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stripe-js.d.ts","sourceRoot":"","sources":["../src/stripe-js.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE5D;;;;;;;;;;GAUG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,CAAC,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC,CAAC;AAI3F,wBAAgB,YAAY,CAAC,GAAG,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAU9E;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAEpC"}
@@ -0,0 +1,24 @@
1
+ import { loadStripe } from '@stripe/stripe-js';
2
+ let cached = null;
3
+ export function loadStripeJs(key) {
4
+ cached ??= (async () => {
5
+ const value = typeof key === 'function' ? await key() : key;
6
+ if (!value)
7
+ return null;
8
+ return loadStripe(value);
9
+ })().catch((err) => {
10
+ cached = null;
11
+ throw err;
12
+ });
13
+ return cached;
14
+ }
15
+ /**
16
+ * Forget the memoised load.
17
+ *
18
+ * For tests, and for an app that can switch accounts without a reload. Nothing
19
+ * in a normal page lifetime needs it.
20
+ */
21
+ export function resetStripeJs() {
22
+ cached = null;
23
+ }
24
+ //# sourceMappingURL=stripe-js.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stripe-js.js","sourceRoot":"","sources":["../src/stripe-js.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAe,MAAM,mBAAmB,CAAC;AAe5D,IAAI,MAAM,GAAkC,IAAI,CAAC;AAEjD,MAAM,UAAU,YAAY,CAAC,GAAyB;IACpD,MAAM,KAAK,CAAC,KAAK,IAAI,EAAE;QACrB,MAAM,KAAK,GAAG,OAAO,GAAG,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;QAC5D,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QAC1B,MAAM,GAAG,IAAI,CAAC;QACd,MAAM,GAAG,CAAC;IACZ,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,GAAG,IAAI,CAAC;AAChB,CAAC"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * What the checkout can come to, and what the caller is handed back.
3
+ *
4
+ * Three outcomes rather than a boolean, because "the buyer closed it" and "we
5
+ * are broken" need different screens. A caller that collapses them tells
6
+ * somebody whose card was declined that payments are down.
7
+ */
8
+ export type CheckoutOutcome = 'completed' | 'closed' | 'unavailable';
9
+ /** What a confirmation attempt came to. `retry` means: build a new charge. */
10
+ export type ConfirmResult = {
11
+ outcome: CheckoutOutcome;
12
+ /** Ready to show. Stripe's own wording where it is the buyer's to fix. */
13
+ error?: string;
14
+ /** The intent is spent — the caller must create another before trying again. */
15
+ retry?: boolean;
16
+ /**
17
+ * The card a SETUP just stored.
18
+ *
19
+ * Carried because attaching a card does not make it billable: Stripe charges
20
+ * the subscription's default payment method, then the customer's, and a card
21
+ * that is neither is simply on file. A caller that added a card BECAUSE the
22
+ * one on file was refused has to be able to say which one to use — otherwise
23
+ * the retry goes back to the card that just failed.
24
+ */
25
+ cardId?: string;
26
+ };
27
+ /**
28
+ * A charge that does not exist yet, described well enough to draw its form.
29
+ *
30
+ * Stripe calls this deferred intent creation, and it is what lets the checkout
31
+ * be ONE screen: the card form, the wallets and the total are on screen from
32
+ * the start, and the charge is created at the moment Pay is pressed rather than
33
+ * to make a form appear. Creating it first means every buyer who got as far as
34
+ * looking at a card form has already left an incomplete subscription behind.
35
+ *
36
+ * `recurring` picks the mode: something that renews is a `subscription` whose
37
+ * card must be usable again unattended; a one-off is a `payment` where keeping
38
+ * the card is the buyer's own choice in the element's checkbox.
39
+ */
40
+ export type DeferredCharge = {
41
+ /** Minor units and currency, exactly as the server quoted them. */
42
+ amount: number;
43
+ currency: string;
44
+ recurring: boolean;
45
+ };
46
+ /** What the form is for: a secret to confirm, or a charge not yet created. */
47
+ export type MountIntent = {
48
+ secret: string;
49
+ kind?: 'payment' | 'setup';
50
+ } | {
51
+ deferred: DeferredCharge;
52
+ };
53
+ export type MountedCheckout = {
54
+ /**
55
+ * Confirm the payment. Resolves once Stripe has taken it, or says why not.
56
+ *
57
+ * The secret is an argument rather than something the form was built with,
58
+ * because a deferred form is mounted BEFORE the charge exists. Omitted for a
59
+ * form that already has one.
60
+ */
61
+ confirm: (clientSecret?: string) => Promise<ConfirmResult>;
62
+ /**
63
+ * Validate and collect, before anything is created. `null` means good to go;
64
+ * a string is Stripe's own complaint about the form, ready to show.
65
+ *
66
+ * Required by Stripe on a deferred form and harmless on any other: it is what
67
+ * turns "the buyer typed something" into payment details Stripe will accept,
68
+ * and it is where an incomplete card is caught — a press that reaches the
69
+ * server would otherwise leave a charge behind for a card never filled in.
70
+ */
71
+ submit: () => Promise<string | null>;
72
+ /** Re-price a deferred form when the total changes. */
73
+ update: (charge: DeferredCharge) => Promise<void>;
74
+ /** Whether the form is filled in far enough to pay. */
75
+ complete: () => boolean;
76
+ destroy: () => void;
77
+ };
78
+ /**
79
+ * A wallet's own path to a charge, and the one thing that still holds it back.
80
+ *
81
+ * Apple Pay, Google Pay and Link confirm from inside their sheet, so they never
82
+ * touch the caller's Pay button — and on a deferred form they need the same
83
+ * steps it takes: submit, create the charge, confirm. `create` is that middle
84
+ * one.
85
+ *
86
+ * `ready` answers a narrow question: is there a settled price to charge? A
87
+ * buyer who changes country and taps Apple Pay while the re-quote is still in
88
+ * flight would have the charge raised at the new total against a form still
89
+ * carrying the old one, and Stripe refuses a confirmation whose amounts
90
+ * disagree.
91
+ */
92
+ export type WalletHooks = {
93
+ ready: () => boolean;
94
+ onNotReady: () => void;
95
+ create: () => Promise<string | null>;
96
+ /**
97
+ * Which wallets this visitor has, once Stripe has decided — empty when none.
98
+ *
99
+ * An empty wallet row and a wallet row still loading look identical from
100
+ * outside the iframe, and guessing between them is how a paywall ends up
101
+ * either flashing a gap or holding a spinner for ever.
102
+ */
103
+ onReady?: (available: string[]) => void;
104
+ };
105
+ /** The address the caller already collected, so Stripe stops asking for it. */
106
+ export type BillingAddress = {
107
+ country: string;
108
+ postalCode?: string;
109
+ };
110
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,QAAQ,GAAG,aAAa,CAAC;AAErE,8EAA8E;AAC9E,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,eAAe,CAAC;IACzB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,8EAA8E;AAC9E,MAAM,MAAM,WAAW,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,CAAA;CAAE,GAAG;IAAE,QAAQ,EAAE,cAAc,CAAA;CAAE,CAAC;AAExG,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;;;OAMG;IACH,OAAO,EAAE,CAAC,YAAY,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;IAC3D;;;;;;;;OAQG;IACH,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACrC,uDAAuD;IACvD,MAAM,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,uDAAuD;IACvD,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,MAAM,OAAO,CAAC;IACrB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACrC;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACzC,CAAC;AAEF,+EAA+E;AAC/E,MAAM,MAAM,cAAc,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "@n1xv/stripe-elements",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Stripe Elements on your own checkout screen: deferred charges, wallet row, saved cards, 3D Secure, and one vocabulary for what a failure means",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "peerDependencies": {
17
+ "@stripe/stripe-js": ">=9"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/n1xv/saas-kit.git",
22
+ "directory": "packages/stripe-elements"
23
+ },
24
+ "keywords": [
25
+ "stripe",
26
+ "stripe-elements",
27
+ "checkout",
28
+ "paywall",
29
+ "apple-pay",
30
+ "google-pay"
31
+ ],
32
+ "devDependencies": {
33
+ "@stripe/stripe-js": "^10.1.0",
34
+ "jsdom": "^30.1.3"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.build.json",
38
+ "typecheck": "tsc --noEmit",
39
+ "lint": "echo 'lint runs from the repo root'",
40
+ "test": "vitest run --passWithNoTests"
41
+ }
6
42
  }