@tuppence/js 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,9 @@
1
+ export { appearanceFor } from "./appearance.js";
2
+ export { TuppenceError } from "./errors.js";
3
+ export type { AddressChange, AddressField, AddressOptions, AddressValue, ExpressCheckout, ExpressCheckoutOptions, ExpressConfirm, } from "./express.js";
4
+ export type { Fetch } from "./http.js";
5
+ export { stripeLocale } from "./locale.js";
6
+ export type { PaymentSheet, PaymentSheetOptions, SetupSheet, SetupSheetOptions, SheetChange, SheetOptions, } from "./sheet.js";
7
+ export { type CartAddress, type CartDeliveryOption, type CartTotals, type StorefrontCart, StorefrontCartClient, type StorefrontOptions, } from "./storefront.js";
8
+ export { type BankAccountDetailsResult, type ConfirmPaymentOptions, type ConfirmSetupOptions, createTuppence, type ElementsOptions, type LoadStripe, loadTuppence, type Payable, type PaymentResult, type SetupResult, Tuppence, type TuppenceOptions, } from "./tuppence.js";
9
+ export type { BankAccountDetails, Branding, ExternalAccountParams, Gateway, LastError, NextAction, PaymentProcessing, PaymentStatus, PublicLinkTopup, PublicPayment, PublicSetupSession, PublicTopupLinkPage, SdkConfig, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { appearanceFor } from "./appearance.js";
2
+ export { TuppenceError } from "./errors.js";
3
+ export { stripeLocale } from "./locale.js";
4
+ export { StorefrontCartClient, } from "./storefront.js";
5
+ export { createTuppence, loadTuppence, Tuppence, } from "./tuppence.js";
@@ -0,0 +1,12 @@
1
+ import type { StripeElementLocale } from "@stripe/stripe-js";
2
+ /**
3
+ * The locale Stripe.js is loaded with for a payer's language (100-18), a BCP 47 tag such as
4
+ * `de-AT`: Stripe's own variant where it has one (`en-GB`, `fr-CA`, `pt-BR`), else the language
5
+ * (`de-AT` is `de`), else English. With no language at all, Stripe decides (`auto`).
6
+ *
7
+ * Stripe's `pt` is Brazilian Portuguese, so a payer in Portugal (`pt-PT`, or a bare `pt`) gets
8
+ * Stripe's English rather than Brazilian wording; only `pt-BR` gets Stripe's Portuguese.
9
+ */
10
+ export declare function stripeLocale(tag: string | null | undefined): StripeElementLocale;
11
+ /** The browser's language, where there is a browser. */
12
+ export declare function browserLanguage(): string | undefined;
package/dist/locale.js ADDED
@@ -0,0 +1,79 @@
1
+ /** The locales Stripe.js and its Elements speak (Stripe's `StripeElementLocale`, less `auto`). */
2
+ const STRIPE_LOCALES = new Set([
3
+ "ar",
4
+ "bg",
5
+ "cs",
6
+ "da",
7
+ "de",
8
+ "el",
9
+ "en",
10
+ "en-AU",
11
+ "en-CA",
12
+ "en-NZ",
13
+ "en-GB",
14
+ "es",
15
+ "es-ES",
16
+ "es-419",
17
+ "et",
18
+ "fi",
19
+ "fil",
20
+ "fr",
21
+ "fr-CA",
22
+ "fr-FR",
23
+ "he",
24
+ "hu",
25
+ "hr",
26
+ "id",
27
+ "it",
28
+ "it-IT",
29
+ "ja",
30
+ "ko",
31
+ "lt",
32
+ "lv",
33
+ "ms",
34
+ "mt",
35
+ "nb",
36
+ "nl",
37
+ "no",
38
+ "pl",
39
+ "pt",
40
+ "pt-BR",
41
+ "ro",
42
+ "ru",
43
+ "sk",
44
+ "sl",
45
+ "sv",
46
+ "th",
47
+ "tr",
48
+ "vi",
49
+ "zh",
50
+ "zh-HK",
51
+ "zh-TW",
52
+ ]);
53
+ /**
54
+ * The locale Stripe.js is loaded with for a payer's language (100-18), a BCP 47 tag such as
55
+ * `de-AT`: Stripe's own variant where it has one (`en-GB`, `fr-CA`, `pt-BR`), else the language
56
+ * (`de-AT` is `de`), else English. With no language at all, Stripe decides (`auto`).
57
+ *
58
+ * Stripe's `pt` is Brazilian Portuguese, so a payer in Portugal (`pt-PT`, or a bare `pt`) gets
59
+ * Stripe's English rather than Brazilian wording; only `pt-BR` gets Stripe's Portuguese.
60
+ */
61
+ export function stripeLocale(tag) {
62
+ const parts = (tag ?? "").trim().replace(/_/g, "-").split("-").filter(Boolean);
63
+ const language = parts[0]?.toLowerCase();
64
+ if (!language || !/^[a-z]{2,3}$/.test(language))
65
+ return "auto";
66
+ // The region is the two-letter or three-digit subtag (a script such as `Hant` is skipped).
67
+ const region = parts.slice(1).find((p) => /^([A-Za-z]{2}|\d{3})$/.test(p));
68
+ const variant = region ? `${language}-${region.toUpperCase()}` : null;
69
+ if (language === "pt")
70
+ return variant === "pt-BR" ? "pt-BR" : "en";
71
+ if (variant && STRIPE_LOCALES.has(variant))
72
+ return variant;
73
+ return STRIPE_LOCALES.has(language) ? language : "en";
74
+ }
75
+ /** The browser's language, where there is a browser. */
76
+ export function browserLanguage() {
77
+ const nav = globalThis.navigator;
78
+ return typeof nav?.language === "string" && nav.language ? nav.language : undefined;
79
+ }
@@ -0,0 +1,92 @@
1
+ import type { Appearance, ConfirmationTokenCreateParams, StripeElementLocale, StripeElements, StripePaymentElement } from "@stripe/stripe-js";
2
+ import { TuppenceError } from "./errors.js";
3
+ import type { PaymentResult, SetupResult, Tuppence } from "./tuppence.js";
4
+ import type { PublicPayment, PublicSetupSession } from "./types.js";
5
+ /** What a sheet tells the page: its fields are showing, their state, or why they cannot show. */
6
+ export type SheetChange = {
7
+ complete: boolean;
8
+ empty: boolean;
9
+ };
10
+ type Handlers = {
11
+ ready: () => void;
12
+ change: (change: SheetChange) => void;
13
+ error: (error: TuppenceError) => void;
14
+ };
15
+ export interface SheetOptions {
16
+ /** The object's client secret, from your server. */
17
+ clientSecret: string;
18
+ /** How the fields look: the business's branding on the payer's view unless you say. */
19
+ appearance?: Appearance;
20
+ /** Apple Pay and Google Pay where the device has them (`auto`, the default), or `never`. */
21
+ wallets?: "auto" | "never";
22
+ /** Stripe's layout for the payment methods: `tabs` (the default) or `accordion`. */
23
+ layout?: "tabs" | "accordion";
24
+ /**
25
+ * The language Stripe's fields speak, one of Stripe.js's locales (`de`, `fr`, `en-GB`…): the
26
+ * browser's (`auto`, Stripe's default) unless you say. Your payer's language on your own page.
27
+ */
28
+ locale?: StripeElementLocale;
29
+ }
30
+ export interface PaymentSheetOptions extends SheetOptions {
31
+ /** The payment, as `retrievePayment` answered it — read when the sheet mounts if not given. */
32
+ payment?: PublicPayment;
33
+ /** What is charged, when the page adds to it (a tip) — the payment's `amount` otherwise. */
34
+ amount?: number;
35
+ }
36
+ export interface SetupSheetOptions extends SheetOptions {
37
+ setupSession?: PublicSetupSession;
38
+ }
39
+ type Mounted = {
40
+ elements: StripeElements;
41
+ element: StripePaymentElement;
42
+ };
43
+ /** A sheet: Stripe's Payment Element, mounted where the page says, confirmed through Tuppence. */
44
+ declare abstract class Sheet<Result> {
45
+ protected readonly tuppence: Tuppence;
46
+ protected readonly options: SheetOptions;
47
+ private handlers;
48
+ protected mounted: Promise<Mounted | null> | null;
49
+ /** Taken down: a load still under way never mounts, and nothing is told any more. */
50
+ private destroyed;
51
+ constructor(tuppence: Tuppence, options: SheetOptions);
52
+ /** The card fields' group, once mounted: for a page that needs Stripe's own methods. */
53
+ protected abstract elementsFor(): Promise<StripeElements | null>;
54
+ abstract confirm(options?: {
55
+ params?: ConfirmationTokenCreateParams;
56
+ }): Promise<Result>;
57
+ on<E extends keyof Handlers>(event: E, handler: Handlers[E]): this;
58
+ /** Shows the fields in `target` (an element, or a selector). Loads Stripe.js if it must. */
59
+ mount(target: string | HTMLElement): this;
60
+ /**
61
+ * Takes the fields off the page — or, while they are still loading, keeps them off it — and
62
+ * says nothing more. The sheet can be mounted again.
63
+ */
64
+ destroy(): void;
65
+ protected emit<E extends keyof Handlers>(event: E, ...detail: Parameters<Handlers[E]>): void;
66
+ protected ready(): Promise<Mounted>;
67
+ private load;
68
+ }
69
+ /** Stripe's Payment Element for a payment: `tuppence.paymentSheet({ clientSecret }).mount(el)`. */
70
+ export declare class PaymentSheet extends Sheet<PaymentResult> {
71
+ private readonly paymentOptions;
72
+ private amount;
73
+ constructor(tuppence: Tuppence, paymentOptions: PaymentSheetOptions);
74
+ protected elementsFor(): Promise<StripeElements | null>;
75
+ /** What is charged changed (a tip): the fields — and Apple Pay's sheet — show the new total. */
76
+ setAmount(amount: number): void;
77
+ /** Pays: the steps of `confirmPayment`, with the fields this sheet shows. */
78
+ confirm(options?: {
79
+ tipAmount?: number;
80
+ params?: ConfirmationTokenCreateParams;
81
+ }): Promise<PaymentResult>;
82
+ }
83
+ /** Stripe's Payment Element for saving a card with nothing charged. */
84
+ export declare class SetupSheet extends Sheet<SetupResult> {
85
+ private readonly setupOptions;
86
+ constructor(tuppence: Tuppence, setupOptions: SetupSheetOptions);
87
+ protected elementsFor(): Promise<StripeElements | null>;
88
+ confirm(options?: {
89
+ params?: ConfirmationTokenCreateParams;
90
+ }): Promise<SetupResult>;
91
+ }
92
+ export {};
package/dist/sheet.js ADDED
@@ -0,0 +1,136 @@
1
+ import { appearanceFor } from "./appearance.js";
2
+ import { TuppenceError } from "./errors.js";
3
+ /** No card fields where no Stripe is behind the environment. */
4
+ const simulated = () => new TuppenceError({
5
+ type: "invalid_request_error",
6
+ code: "no_card_fields",
7
+ message: "There are no card fields here: this environment runs the simulated gateway. Pay with `testPaymentMethod` instead.",
8
+ });
9
+ /** A sheet: Stripe's Payment Element, mounted where the page says, confirmed through Tuppence. */
10
+ class Sheet {
11
+ constructor(tuppence, options) {
12
+ this.tuppence = tuppence;
13
+ this.options = options;
14
+ this.handlers = {
15
+ ready: [],
16
+ change: [],
17
+ error: [],
18
+ };
19
+ this.mounted = null;
20
+ /** Taken down: a load still under way never mounts, and nothing is told any more. */
21
+ this.destroyed = false;
22
+ }
23
+ on(event, handler) {
24
+ this.handlers[event].push(handler);
25
+ return this;
26
+ }
27
+ /** Shows the fields in `target` (an element, or a selector). Loads Stripe.js if it must. */
28
+ mount(target) {
29
+ this.destroyed = false;
30
+ this.mounted ?? (this.mounted = this.load(target).catch((caught) => {
31
+ this.emit("error", caught instanceof TuppenceError
32
+ ? caught
33
+ : new TuppenceError({ type: "api_error", message: String(caught) }));
34
+ return null;
35
+ }));
36
+ return this;
37
+ }
38
+ /**
39
+ * Takes the fields off the page — or, while they are still loading, keeps them off it — and
40
+ * says nothing more. The sheet can be mounted again.
41
+ */
42
+ destroy() {
43
+ this.destroyed = true;
44
+ const mounted = this.mounted;
45
+ this.mounted = null;
46
+ void mounted?.then((m) => m?.element.destroy());
47
+ }
48
+ emit(event, ...detail) {
49
+ if (this.destroyed)
50
+ return;
51
+ for (const handler of this.handlers[event])
52
+ handler(...detail);
53
+ }
54
+ async ready() {
55
+ const mounted = await this.mounted;
56
+ if (!mounted) {
57
+ throw new TypeError("Mount the sheet, and let its fields load, before confirming.");
58
+ }
59
+ return mounted;
60
+ }
61
+ async load(target) {
62
+ const elements = await this.elementsFor();
63
+ // Taken down while Stripe.js loaded (a page that moved on, React's double mount in dev).
64
+ if (this.destroyed)
65
+ return null;
66
+ if (!elements) {
67
+ this.emit("error", simulated());
68
+ return null;
69
+ }
70
+ const wallet = this.options.wallets ?? "auto";
71
+ const element = elements.create("payment", {
72
+ layout: this.options.layout ?? "tabs",
73
+ wallets: { applePay: wallet, googlePay: wallet },
74
+ });
75
+ element.on("ready", () => this.emit("ready"));
76
+ element.on("change", (event) => this.emit("change", { complete: event.complete, empty: event.empty }));
77
+ element.on("loaderror", (event) => this.emit("error", TuppenceError.fromStripe(event.error)));
78
+ element.mount(target);
79
+ return { elements, element };
80
+ }
81
+ }
82
+ /** Stripe's Payment Element for a payment: `tuppence.paymentSheet({ clientSecret }).mount(el)`. */
83
+ export class PaymentSheet extends Sheet {
84
+ constructor(tuppence, paymentOptions) {
85
+ super(tuppence, paymentOptions);
86
+ this.paymentOptions = paymentOptions;
87
+ this.amount = paymentOptions.amount;
88
+ }
89
+ async elementsFor() {
90
+ const payment = this.paymentOptions.payment ??
91
+ (await this.tuppence.retrievePayment(this.paymentOptions.clientSecret));
92
+ return this.tuppence.elements(payment, {
93
+ appearance: this.paymentOptions.appearance ?? appearanceFor(payment.branding),
94
+ ...(this.paymentOptions.locale ? { locale: this.paymentOptions.locale } : {}),
95
+ ...(this.amount === undefined ? {} : { amount: this.amount }),
96
+ });
97
+ }
98
+ /** What is charged changed (a tip): the fields — and Apple Pay's sheet — show the new total. */
99
+ setAmount(amount) {
100
+ this.amount = amount;
101
+ void this.mounted?.then((m) => m?.elements.update({ amount }));
102
+ }
103
+ /** Pays: the steps of `confirmPayment`, with the fields this sheet shows. */
104
+ async confirm(options = {}) {
105
+ const { elements } = await this.ready();
106
+ return this.tuppence.confirmPayment({
107
+ clientSecret: this.paymentOptions.clientSecret,
108
+ elements,
109
+ ...(options.tipAmount === undefined ? {} : { tipAmount: options.tipAmount }),
110
+ ...(options.params ? { params: options.params } : {}),
111
+ });
112
+ }
113
+ }
114
+ /** Stripe's Payment Element for saving a card with nothing charged. */
115
+ export class SetupSheet extends Sheet {
116
+ constructor(tuppence, setupOptions) {
117
+ super(tuppence, setupOptions);
118
+ this.setupOptions = setupOptions;
119
+ }
120
+ async elementsFor() {
121
+ const session = this.setupOptions.setupSession ??
122
+ (await this.tuppence.retrieveSetupSession(this.setupOptions.clientSecret));
123
+ return this.tuppence.setupElements(session, {
124
+ appearance: this.setupOptions.appearance ?? appearanceFor(session.branding),
125
+ ...(this.setupOptions.locale ? { locale: this.setupOptions.locale } : {}),
126
+ });
127
+ }
128
+ async confirm(options = {}) {
129
+ const { elements } = await this.ready();
130
+ return this.tuppence.confirmSetup({
131
+ clientSecret: this.setupOptions.clientSecret,
132
+ elements,
133
+ ...(options.params ? { params: options.params } : {}),
134
+ });
135
+ }
136
+ }
@@ -0,0 +1,90 @@
1
+ import { type Fetch } from "./http.js";
2
+ /** Where a store's basket is (plan/85 §Basket): the routes under `/storefront/v1/cart`. */
3
+ export interface StorefrontOptions {
4
+ /**
5
+ * The store's address, before `/storefront/v1/…`: `""` on the store's own domain (the
6
+ * default), `/<slug>` or `/test/<slug>` on the shared address, or Tuppence's API headless.
7
+ * On the store's own hosts the basket's cookie rides along; headless it never does.
8
+ */
9
+ base?: string;
10
+ /** Headless only: the account's publishable key. */
11
+ publishableKey?: string;
12
+ /** Headless only: the basket's token, as the call that made the basket answered it. */
13
+ cartToken?: string;
14
+ }
15
+ /** Where an order goes, as far as pricing needs: the wallet sheet's address is only this much. */
16
+ export interface CartAddress {
17
+ /** ISO 3166-1 alpha-2: `GB`, `US`. */
18
+ country: string;
19
+ /** A US state or other subdivision's code: `TX`. */
20
+ region?: string | null;
21
+ postcode?: string | null;
22
+ }
23
+ /** A delivery or collection choice with its promise (`YYYY-MM-DD` in the shop's zone). */
24
+ export interface CartDeliveryOption {
25
+ /**
26
+ * What to send back as `delivery` to choose it. Opaque: on shipping profiles it joins one rate
27
+ * per profile in the basket, so it can run to a few thousand characters.
28
+ */
29
+ key: string;
30
+ name: string;
31
+ /** `delivery` or `collection`. */
32
+ kind: string;
33
+ amount: number;
34
+ earliest: string;
35
+ latest: string;
36
+ /** The day it leaves (collection: the day it is ready), `YYYY-MM-DD`. */
37
+ dispatch_by: string;
38
+ /** What to show: "Arrives Fri 4 to Sat 5 Sep", "Ready to collect Wednesday 2 September". */
39
+ label: string;
40
+ selected: boolean;
41
+ }
42
+ /** The basket's figures, every one the server's (store-core on the till's engine). */
43
+ export interface CartTotals {
44
+ subtotal: number;
45
+ discount: number;
46
+ delivery: number;
47
+ tax: number;
48
+ prices_include_tax: boolean;
49
+ total: number;
50
+ gift_cards: number;
51
+ /** What the shopper pays by card or wallet. */
52
+ due: number;
53
+ }
54
+ /** The basket, as `GET /storefront/v1/cart` and `POST …/quote` answer it. */
55
+ export interface StorefrontCart {
56
+ object: "cart";
57
+ id: string | null;
58
+ livemode: boolean;
59
+ currency: string;
60
+ status: string;
61
+ count: number;
62
+ lines: Array<{
63
+ id: string;
64
+ name: string;
65
+ quantity: number;
66
+ amount: number;
67
+ [f: string]: unknown;
68
+ }>;
69
+ totals: CartTotals | null;
70
+ delivery_options: CartDeliveryOption[];
71
+ /** Why nothing can be delivered here, in words for the shopper. */
72
+ delivery_refusal: {
73
+ code: string;
74
+ message: string;
75
+ } | null;
76
+ [field: string]: unknown;
77
+ }
78
+ /** A store's basket routes: reads, and the quote the wallet sheet asks for. */
79
+ export declare class StorefrontCartClient {
80
+ private readonly http;
81
+ private readonly headers;
82
+ constructor(fetchImpl: Fetch, options?: StorefrontOptions);
83
+ /** The shopper's basket on this store, priced. */
84
+ get(): Promise<StorefrontCart>;
85
+ /** The totals for an address and a delivery choice, changing nothing. */
86
+ quote(input: {
87
+ address?: CartAddress | null;
88
+ delivery?: string | null;
89
+ }): Promise<StorefrontCart>;
90
+ }
@@ -0,0 +1,25 @@
1
+ import { Http } from "./http.js";
2
+ /** A store's basket routes: reads, and the quote the wallet sheet asks for. */
3
+ export class StorefrontCartClient {
4
+ constructor(fetchImpl, options = {}) {
5
+ const base = (options.base ?? "").replace(/\/+$/, "");
6
+ const headless = /^https?:\/\//.test(base);
7
+ // Same origin: the store's HttpOnly cookie is the basket. Headless: headers, no cookie.
8
+ this.http = new Http(base, fetchImpl, undefined, headless ? "omit" : "same-origin");
9
+ this.headers = {
10
+ ...(options.publishableKey ? { "Tuppence-Publishable-Key": options.publishableKey } : {}),
11
+ ...(options.cartToken ? { "Tuppence-Cart": options.cartToken } : {}),
12
+ };
13
+ }
14
+ /** The shopper's basket on this store, priced. */
15
+ get() {
16
+ return this.http.get("/storefront/v1/cart", this.headers);
17
+ }
18
+ /** The totals for an address and a delivery choice, changing nothing. */
19
+ quote(input) {
20
+ return this.http.post("/storefront/v1/cart/quote", this.headers, {
21
+ address: input.address ?? null,
22
+ delivery: input.delivery ?? null,
23
+ });
24
+ }
25
+ }