@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,484 @@
1
+ import { externalAccountParams } from "./bank-details.js";
2
+ import { TuppenceError } from "./errors.js";
3
+ import { AddressField, ExpressCheckout, } from "./express.js";
4
+ import { Http } from "./http.js";
5
+ import { browserLanguage, stripeLocale } from "./locale.js";
6
+ import { PaymentSheet, SetupSheet, } from "./sheet.js";
7
+ import { StorefrontCartClient } from "./storefront.js";
8
+ const DEFAULT_API = "https://api.tuppence.ai";
9
+ /** A publishable key travels in this header, never in a URL (a URL is logged, cached, shared). */
10
+ const PUBLISHABLE_KEY_HEADER = "Tuppence-Publishable-Key";
11
+ const CLIENT_SECRET_HEADER = "Tuppence-Client-Secret";
12
+ /** Where Stripe may know more than Tuppence does: the payer's bank is checking, or Stripe is. */
13
+ const WAITING = new Set(["requires_action", "processing"]);
14
+ /**
15
+ * Still worth reading back: the bank is checking, or a CARD is processing (seconds, rarely). A
16
+ * bank debit that is `processing` — a Bacs Direct Debit, `payment_method_type` other than `card`
17
+ * — takes working days: that IS the answer, with `processing.expected_debit_date`, and nothing
18
+ * waits for `succeeded`.
19
+ */
20
+ const stillWaiting = (current) => WAITING.has(current.status) &&
21
+ !(current.status === "processing" &&
22
+ typeof current.payment_method_type === "string" &&
23
+ current.payment_method_type !== "card");
24
+ /** A hosted top-up page's address, as `POST /v1/issuing/topup_links` answers it (54-18). */
25
+ const TOPUP_PAGE = /^\/card-topup\/[A-Za-z0-9]{20,64}\/?$/;
26
+ const LOCAL = new Set(["localhost", "127.0.0.1", "[::1]"]);
27
+ /** `pay_…_secret_…` → `pay_…`: the object a client secret belongs to, or a clear refusal. */
28
+ function objectOf(clientSecret, prefix) {
29
+ const secret = typeof clientSecret === "string" ? clientSecret : "";
30
+ const at = secret.indexOf("_secret_");
31
+ const id = at > 0 ? secret.slice(0, at) : "";
32
+ if (!id.startsWith(prefix) || !/^[A-Za-z0-9_]+$/.test(id)) {
33
+ const what = prefix === "pay_" ? "a payment" : "a setup session";
34
+ throw new TypeError(`Expected the client secret of ${what} (${prefix}…_secret_…).`);
35
+ }
36
+ return id;
37
+ }
38
+ const secretHeader = (clientSecret) => ({ [CLIENT_SECRET_HEADER]: clientSecret });
39
+ /** The bank refused the card: the object says why, in words written for the payer. */
40
+ const refused = (last) => new TuppenceError({
41
+ type: "card_error",
42
+ message: last.message ?? "The card was declined.",
43
+ code: last.code,
44
+ declineCode: last.decline_code,
45
+ status: 402,
46
+ });
47
+ /**
48
+ * No answer, a confirm already in flight, or a server error: the card MAY have been presented.
49
+ * Such an outcome is read back — the card is never presented twice.
50
+ */
51
+ const unknownOutcome = (error) => error.status === 0 || error.status === 409 || error.status >= 500;
52
+ /**
53
+ * Takes a card in the browser for a Tuppence payment — through Stripe's own fields, on the
54
+ * business's own Stripe account. Card details go from the payer's device to Stripe and never
55
+ * through Tuppence, this code, or yours.
56
+ */
57
+ export class Tuppence {
58
+ constructor(options = {},
59
+ /** What `loadTuppence` learned about its key; null for a page that only holds client secrets. */
60
+ config = null) {
61
+ this.config = config;
62
+ this.stripes = new Map();
63
+ /** The Stripe instance each set of card fields came from: its token must come from it too. */
64
+ this.owners = new WeakMap();
65
+ /**
66
+ * The card balance's hosted top-up page (54-18). Your server makes a link
67
+ * (`POST /v1/issuing/topup_links`, with your secret key) and hands its `url` to the page;
68
+ * `tuppence.topups.redirect({ url })` sends the person there — to top up by card or by bank
69
+ * transfer, in your (or your Connect platform's) branding — and they come back to the link's
70
+ * `return_url`. Only a Tuppence top-up page's address is followed: anything else throws.
71
+ */
72
+ this.topups = {
73
+ /**
74
+ * Stripe's card fields for a hosted top-up page's card (54-18), to charge `amount` (the
75
+ * card option's `total`). Null where no Stripe is behind the environment: there,
76
+ * `pay({ testPaymentMethod })` names one of Stripe's test cards.
77
+ */
78
+ elements: async (page, options) => {
79
+ const stripe = await this.stripeFor(page);
80
+ if (!stripe)
81
+ return null;
82
+ const { amount, ...look } = options;
83
+ const elements = stripe.elements({
84
+ ...look,
85
+ mode: "payment",
86
+ amount,
87
+ currency: page.currency,
88
+ captureMethod: "automatic",
89
+ paymentMethodTypes: ["card"],
90
+ });
91
+ this.owners.set(elements, stripe);
92
+ return elements;
93
+ },
94
+ /**
95
+ * Tops up by card on a hosted top-up page (54-18): the card presented (Stripe's fields make a
96
+ * confirmation token), the top-up made, the bank's check in Stripe's own frame if it asks,
97
+ * then the page read back until the bank and Stripe have answered. `businessFunds: true` is
98
+ * the person's word that the card is the business's own money.
99
+ */
100
+ pay: async (options) => {
101
+ const path = `/public/issuing_topup_links/${encodeURIComponent(options.token)}`;
102
+ const presented = await this.present({
103
+ clientSecret: "",
104
+ elements: options.elements,
105
+ testPaymentMethod: options.testPaymentMethod,
106
+ }, false);
107
+ if ("error" in presented) {
108
+ return { page: await this.http.get(path, {}), error: presented.error };
109
+ }
110
+ let page = await this.http.post(`${path}/topups`, {}, {
111
+ ...presented.body,
112
+ business_funds: options.businessFunds,
113
+ ...(options.amount === undefined ? {} : { amount: options.amount }),
114
+ });
115
+ const secret = page.topup?.status === "requires_action" ? page.topup.client_secret : null;
116
+ const stripe = secret ? await this.stripeFor(page) : null;
117
+ // The simulated gateway has no bank page of its own: the caller shows its pretend one.
118
+ if (!secret || !stripe)
119
+ return { page };
120
+ const handled = await stripe.handleNextAction({ clientSecret: secret });
121
+ for (let waited = 0; waited < this.timeoutMs; waited += this.intervalMs) {
122
+ page = await this.http.get(path, {});
123
+ if (page.topup?.status !== "requires_action")
124
+ break;
125
+ await this.sleep(this.intervalMs);
126
+ }
127
+ return handled.error ? { page, error: TuppenceError.fromStripe(handled.error) } : { page };
128
+ },
129
+ /**
130
+ * Finishes the bank's check on a hosted page's card top-up that waited for it when the page
131
+ * opened (a reload half-way), in Stripe's own frame, then reads the page back.
132
+ */
133
+ resume: async (options) => {
134
+ const path = `/public/issuing_topup_links/${encodeURIComponent(options.token)}`;
135
+ let page = await this.http.get(path, {});
136
+ const secret = page.topup?.status === "requires_action" ? page.topup.client_secret : null;
137
+ const stripe = secret ? await this.stripeFor(page) : null;
138
+ if (!secret || !stripe)
139
+ return { page };
140
+ const handled = await stripe.handleNextAction({ clientSecret: secret });
141
+ for (let waited = 0; waited < this.timeoutMs; waited += this.intervalMs) {
142
+ page = await this.http.get(path, {});
143
+ if (page.topup?.status !== "requires_action")
144
+ break;
145
+ await this.sleep(this.intervalMs);
146
+ }
147
+ return handled.error ? { page, error: TuppenceError.fromStripe(handled.error) } : { page };
148
+ },
149
+ redirect: (options) => {
150
+ let target = null;
151
+ try {
152
+ target = new URL(typeof options?.url === "string" ? options.url : "");
153
+ }
154
+ catch {
155
+ target = null;
156
+ }
157
+ const secure = target?.protocol === "https:" ||
158
+ (target?.protocol === "http:" && LOCAL.has(target.hostname));
159
+ if (!target || !secure || !TOPUP_PAGE.test(target.pathname) || target.search || target.hash) {
160
+ throw new TypeError("topups.redirect takes a top-up link's `url`, as `POST /v1/issuing/topup_links` answered it (https://…/card-topup/…).");
161
+ }
162
+ this.navigate(target.toString());
163
+ },
164
+ };
165
+ const fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
166
+ this.fetchImpl = fetchImpl;
167
+ this.sleep = options.poll?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
168
+ this.navigate = options.navigate ?? ((url) => globalThis.location.assign(url));
169
+ this.http = new Http((options.apiBase ?? DEFAULT_API).replace(/\/+$/, ""), fetchImpl, this.sleep);
170
+ this.loadStripe =
171
+ options.loadStripe ??
172
+ (async (key, { stripeAccount, locale }) => {
173
+ // Stripe's loader arrives only when card fields are first asked for: a page on the
174
+ // simulated gateway, or one that never shows fields, never downloads it.
175
+ const { loadStripe } = await import("@stripe/stripe-js/pure");
176
+ return loadStripe(key, { stripeAccount, locale });
177
+ });
178
+ this.locale = stripeLocale(options.locale ?? browserLanguage());
179
+ this.intervalMs = options.poll?.intervalMs ?? 1000;
180
+ this.timeoutMs = options.poll?.timeoutMs ?? 30000;
181
+ }
182
+ /** `loadTuppence`: the key checked here, then what the API says about it. */
183
+ static async load(publishableKey, options = {}) {
184
+ const key = typeof publishableKey === "string" ? publishableKey : "";
185
+ // A secret key is refused by its code (`invalid_api_key`, as the API refuses one): every
186
+ // payer-side SDK uses the same code, each in its own words (56-03, A9).
187
+ if (/^(sk|rk)_/.test(key)) {
188
+ throw new TuppenceError({
189
+ type: "authentication_error",
190
+ code: "invalid_api_key",
191
+ message: "That is a secret key: it must never be in a page. Roll it, and pass your publishable key (pk_…) here.",
192
+ });
193
+ }
194
+ if (!/^pk_(test|live)_[A-Za-z0-9]+$/.test(key)) {
195
+ throw new TypeError("loadTuppence needs your publishable key (pk_test_… or pk_live_…).");
196
+ }
197
+ const config = await new Tuppence(options).http.get("/public/sdk/config", {
198
+ [PUBLISHABLE_KEY_HEADER]: key,
199
+ });
200
+ const tuppence = new Tuppence(options, config);
201
+ // Stripe.js starts loading now, so the card fields appear the moment they are asked for.
202
+ void tuppence.stripeFor(config).catch(() => undefined);
203
+ return tuppence;
204
+ }
205
+ /** The payment a client secret belongs to, as its payer sees it. */
206
+ retrievePayment(clientSecret) {
207
+ const id = objectOf(clientSecret, "pay_");
208
+ return this.http.get(`/public/payments/${id}`, secretHeader(clientSecret));
209
+ }
210
+ /** The setup session a client secret belongs to, as its customer sees it. */
211
+ retrieveSetupSession(clientSecret) {
212
+ const id = objectOf(clientSecret, "sets_");
213
+ return this.http.get(`/public/setup_sessions/${id}`, secretHeader(clientSecret));
214
+ }
215
+ /**
216
+ * Stripe's card fields for a payment — `elements.create("payment").mount(el)` shows them. What
217
+ * they collect matches the charge Tuppence will make: its amount, currency, how it is
218
+ * captured and whether it saves the card. Null where no Stripe is behind the environment (the
219
+ * simulated gateway): there, `testPaymentMethod` names one of Stripe's test cards instead.
220
+ *
221
+ * A page that adds to the amount (a tip) says so with `elements.update({ amount })`.
222
+ */
223
+ async elements(payment, options = {}) {
224
+ const stripe = await this.stripeFor(payment);
225
+ if (!stripe)
226
+ return null;
227
+ const { amount, ...look } = options;
228
+ // Stripe.js in this mode offers what the business's settings offer, whatever the
229
+ // PaymentIntent says: what can't be used here (Direct Debit on a booking) is left out.
230
+ const excluded = payment.excluded_payment_method_types ?? [];
231
+ const elements = stripe.elements({
232
+ ...look,
233
+ mode: "payment",
234
+ amount: amount ?? payment.amount,
235
+ currency: payment.currency,
236
+ ...(payment.capture_method ? { captureMethod: payment.capture_method } : {}),
237
+ ...(payment.setup_future_usage ? { setupFutureUsage: payment.setup_future_usage } : {}),
238
+ ...(excluded.length > 0 ? { excludedPaymentMethodTypes: [...excluded] } : {}),
239
+ });
240
+ this.owners.set(elements, stripe);
241
+ return elements;
242
+ }
243
+ /** Stripe's card fields for saving a card with nothing charged — cards only, as the session saves. */
244
+ async setupElements(session, options = {}) {
245
+ const stripe = await this.stripeFor(session);
246
+ if (!stripe)
247
+ return null;
248
+ const elements = stripe.elements({ ...options, mode: "setup", paymentMethodTypes: ["card"] });
249
+ this.owners.set(elements, stripe);
250
+ return elements;
251
+ }
252
+ /**
253
+ * Stripe's Payment Element for a payment, dressed in the business's branding:
254
+ * `tuppence.paymentSheet({ clientSecret }).mount("#card")`, then `sheet.confirm()`. Its events
255
+ * say when the fields show (`ready`), whether they are complete (`change`), and why they
256
+ * cannot show (`error` — on the simulated gateway there are none).
257
+ */
258
+ paymentSheet(options) {
259
+ return new PaymentSheet(this, options);
260
+ }
261
+ /**
262
+ * A store's basket (85-10): `get()` it, or `quote()` it for an address and a delivery choice
263
+ * without changing it. On the store's own hosts the basket is its cookie; headless (M94) pass
264
+ * `base: "https://api.tuppence.ai"`, the publishable key and the basket's token.
265
+ */
266
+ cart(options = {}) {
267
+ return new StorefrontCartClient(this.fetchImpl, options);
268
+ }
269
+ /**
270
+ * Apple Pay, Google Pay and Link for a store's basket (85-10): Stripe's Express Checkout
271
+ * Element in deferred mode, in the business's branding. The sheet's address and delivery
272
+ * choices are priced by the basket's quote, and the total in the sheet follows;
273
+ * `onConfirm` gets Stripe's confirmation token for your server to pay with.
274
+ * `tuppence.expressCheckout(payable, { onConfirm }).mount("#express")`.
275
+ */
276
+ expressCheckout(payable, options) {
277
+ const cart = options.cart instanceof StorefrontCartClient ? options.cart : this.cart(options.cart);
278
+ return new ExpressCheckout(this, payable, options, cart);
279
+ }
280
+ /**
281
+ * Stripe's Address Element, with Stripe's autocomplete (85-10): in a payment's own Elements
282
+ * group (from `elements()`) so a shipping address goes with the payment, or, from a payment
283
+ * alone, in Elements of its own in the business's branding.
284
+ */
285
+ address(from, options) {
286
+ return new AddressField(this, from, options);
287
+ }
288
+ /**
289
+ * Stripe's confirmation token for fields this SDK made: the fields checked, then the token.
290
+ * @internal For the SDK's own wallets; pages pay through `confirmPayment`.
291
+ */
292
+ async confirmationToken(elements, params) {
293
+ const stripe = this.owners.get(elements);
294
+ if (!stripe) {
295
+ throw new TypeError("Pass the card fields this SDK made (`tuppence.elements()`): they carry the account the card is for.");
296
+ }
297
+ const submitted = await elements.submit();
298
+ if (submitted.error)
299
+ return { error: TuppenceError.fromStripe(submitted.error) };
300
+ const token = await stripe.createConfirmationToken({ elements, ...(params ? { params } : {}) });
301
+ if (token.error)
302
+ return { error: TuppenceError.fromStripe(token.error) };
303
+ return { id: token.confirmationToken.id };
304
+ }
305
+ /** The same for saving a card with nothing charged. */
306
+ setupSheet(options) {
307
+ return new SetupSheet(this, options);
308
+ }
309
+ /**
310
+ * Pays a payment: the card is presented (Stripe's fields make a confirmation token), the
311
+ * payer's bank checks it's them if it asks (in Stripe's own frame), and the payment is read
312
+ * back until it settles. A refused card is `{ error }` with the payment as it now stands —
313
+ * never an exception. If an answer is lost on the way, the outcome is read back: the card is
314
+ * never presented twice.
315
+ *
316
+ * A Bacs Direct Debit answers at once with `status: "processing"`, `payment_method_type:
317
+ * "bacs_debit"` and `processing.expected_debit_date` (`YYYY-MM-DD`): the mandate is set up and
318
+ * the money is collected in a few working days. Show the payer that; `succeeded` (or
319
+ * `failed`) comes later, by webhook — `payment.succeeded`, `payment.failed`.
320
+ */
321
+ async confirmPayment(options) {
322
+ const id = objectOf(options.clientSecret, "pay_");
323
+ const presented = await this.present(options, true);
324
+ if ("error" in presented)
325
+ return { error: presented.error, payment: null };
326
+ const body = options.tipAmount === undefined
327
+ ? presented.body
328
+ : { ...presented.body, tip_amount: options.tipAmount };
329
+ const outcome = await this.run(`/public/payments/${id}`, options.clientSecret, body);
330
+ return outcome.error
331
+ ? { error: outcome.error, payment: outcome.value }
332
+ : { payment: outcome.value };
333
+ }
334
+ /** Saves a card with nothing charged — the same steps as a payment. */
335
+ async confirmSetup(options) {
336
+ const id = objectOf(options.clientSecret, "sets_");
337
+ const presented = await this.present(options, false);
338
+ if ("error" in presented)
339
+ return { error: presented.error, setupSession: null };
340
+ const outcome = await this.run(`/public/setup_sessions/${id}`, options.clientSecret, presented.body);
341
+ return outcome.error
342
+ ? { error: outcome.error, setupSession: outcome.value }
343
+ : { setupSession: outcome.value };
344
+ }
345
+ /**
346
+ * Finishes what a payment waits on when the page opens — the payer's bank checking it's them
347
+ * (a charge to a saved card that asked for it, a check left half-way) — in Stripe's own frame,
348
+ * then reads it back. On the simulated gateway it answers the payment as it stands.
349
+ */
350
+ async resumePayment(options) {
351
+ const id = objectOf(options.clientSecret, "pay_");
352
+ const outcome = await this.resume(`/public/payments/${id}`, options.clientSecret);
353
+ return outcome.error
354
+ ? { error: outcome.error, payment: outcome.value }
355
+ : { payment: outcome.value };
356
+ }
357
+ /** The same for a card being saved. */
358
+ async resumeSetup(options) {
359
+ const id = objectOf(options.clientSecret, "sets_");
360
+ const outcome = await this.resume(`/public/setup_sessions/${id}`, options.clientSecret);
361
+ return outcome.error
362
+ ? { error: outcome.error, setupSession: outcome.value }
363
+ : { setupSession: outcome.value };
364
+ }
365
+ async resume(path, clientSecret) {
366
+ const headers = secretHeader(clientSecret);
367
+ try {
368
+ return await this.onward(path, headers, await this.http.get(path, headers));
369
+ }
370
+ catch (caught) {
371
+ const error = caught instanceof TuppenceError
372
+ ? caught
373
+ : new TuppenceError({ type: "api_error", message: String(caught) });
374
+ return { error, value: null };
375
+ }
376
+ }
377
+ /**
378
+ * A bank account, checked and shaped for `POST /v1/accounts/{id}/external_accounts` — the only
379
+ * way to add one (52-09, 52-17): your server sends `externalAccount` on to Tuppence as
380
+ * `external_account`, and Tuppence keeps the details (encrypted, for verification) and has Stripe
381
+ * make the bank account. Tuppence refuses a Stripe bank token. Nothing is sent from
382
+ * here; a wrong field comes back as an error naming it.
383
+ */
384
+ bankAccountDetails(details) {
385
+ return externalAccountParams(details);
386
+ }
387
+ /** Stripe.js for an object's publishable key and account — loaded once for each pair. */
388
+ stripeFor(view) {
389
+ const key = view.stripe_publishable_key;
390
+ const account = view.stripe_account;
391
+ if (!key || !account)
392
+ return Promise.resolve(null);
393
+ const memo = `${key} ${account}`;
394
+ let loading = this.stripes.get(memo);
395
+ if (!loading) {
396
+ loading = this.loadStripe(key, { stripeAccount: account, locale: this.locale });
397
+ this.stripes.set(memo, loading);
398
+ // A script that failed to load may be asked for again.
399
+ loading.catch(() => this.stripes.delete(memo));
400
+ }
401
+ return loading;
402
+ }
403
+ /** A card, as the confirm route takes it: a confirmation token, a saved card, or a test card. */
404
+ async present(options, canUseSavedCard) {
405
+ if (options.elements) {
406
+ const token = await this.confirmationToken(options.elements, options.params);
407
+ if ("error" in token)
408
+ return { error: token.error };
409
+ return { body: { confirmation_token: token.id } };
410
+ }
411
+ if (canUseSavedCard && options.savedCard) {
412
+ return { body: { saved_card: true } };
413
+ }
414
+ if (options.testPaymentMethod)
415
+ return { body: { payment_method: options.testPaymentMethod } };
416
+ throw new TypeError("Present a card: `elements` — or, on the simulated gateway in test mode, `testPaymentMethod`.");
417
+ }
418
+ /** Confirm, then the bank's check if it asks, then read back until it settles. */
419
+ async run(path, clientSecret, body) {
420
+ const headers = secretHeader(clientSecret);
421
+ let current;
422
+ try {
423
+ current = await this.http.post(`${path}/confirm`, headers, body);
424
+ }
425
+ catch (caught) {
426
+ const error = caught instanceof TuppenceError
427
+ ? caught
428
+ : new TuppenceError({ type: "api_error", message: String(caught) });
429
+ if (!unknownOutcome(error)) {
430
+ return { error, value: await this.http.get(path, headers).catch(() => null) };
431
+ }
432
+ current = await this.settle(path, headers).catch(() => null);
433
+ if (!current || current.status === "requires_payment_method") {
434
+ return { error, value: current };
435
+ }
436
+ }
437
+ return this.onward(path, headers, current);
438
+ }
439
+ /** What follows a confirm: the bank's check if it asks, then a read-back until it settles. */
440
+ async onward(path, headers, confirmed) {
441
+ let current = confirmed;
442
+ if (current.status === "requires_action" && current.next_action?.type === "use_stripe_sdk") {
443
+ const stripe = await this.stripeFor(current);
444
+ // The simulated gateway has no bank page of its own: the caller shows its pretend one.
445
+ if (!stripe)
446
+ return { value: current };
447
+ const handled = await stripe.handleNextAction({
448
+ clientSecret: current.next_action.stripe_client_secret,
449
+ });
450
+ current = (await this.settle(path, headers).catch(() => null)) ?? current;
451
+ if (handled.error && current.status === "requires_payment_method") {
452
+ return { error: TuppenceError.fromStripe(handled.error), value: current };
453
+ }
454
+ }
455
+ else if (stillWaiting(current)) {
456
+ current = (await this.settle(path, headers).catch(() => null)) ?? current;
457
+ }
458
+ if (current.status === "requires_payment_method" && current.last_error) {
459
+ return { error: refused(current.last_error), value: current };
460
+ }
461
+ return { value: current };
462
+ }
463
+ /** Reads an object back until neither the bank nor Stripe is still deciding, or time is up. */
464
+ async settle(path, headers) {
465
+ let current = await this.http.get(path, headers);
466
+ for (let waited = 0; stillWaiting(current) && waited < this.timeoutMs;) {
467
+ await this.sleep(this.intervalMs);
468
+ waited += this.intervalMs;
469
+ current = await this.http.get(path, headers);
470
+ }
471
+ return current;
472
+ }
473
+ }
474
+ /** For a page that holds client secrets and no key — Tuppence's own pay pages. */
475
+ export function createTuppence(options = {}) {
476
+ return new Tuppence(options);
477
+ }
478
+ /**
479
+ * Starts the SDK with your PUBLISHABLE key (`pk_test_…` or `pk_live_…`). A secret key is
480
+ * refused here, before it is sent anywhere: it must never be in a page.
481
+ */
482
+ export function loadTuppence(publishableKey, options = {}) {
483
+ return Tuppence.load(publishableKey, options);
484
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * What the browser's routes answer — the fields this SDK reads. Each object carries more (the
3
+ * business's branding, a receipt, a tip…), kept as they come.
4
+ */
5
+ /** Where a payment is: `requires_action` waits on the payer's bank, `processing` on Stripe. */
6
+ export type PaymentStatus = "requires_payment_method" | "requires_action" | "processing" | "requires_capture" | "succeeded" | "canceled";
7
+ /** Which gateway the environment runs: `fake` has no card fields — Stripe's test cards by name. */
8
+ export type Gateway = "fake" | "sandbox" | "live";
9
+ export interface NextAction {
10
+ type: string;
11
+ /** Stripe's own secret for the bank's check, which Stripe.js finishes. */
12
+ stripe_client_secret: string;
13
+ }
14
+ export interface LastError {
15
+ code: string | null;
16
+ decline_code: string | null;
17
+ message: string | null;
18
+ }
19
+ /** The business's look on its payers' pages (plan 09-08): its accent colour, contrast-checked. */
20
+ export interface Branding {
21
+ display_name?: string;
22
+ accent_color?: string | null;
23
+ }
24
+ /** What Stripe.js needs to take a card for this object — null where no Stripe is behind it. */
25
+ interface StripeFields {
26
+ branding?: Branding;
27
+ stripe: Gateway;
28
+ stripe_publishable_key: string | null;
29
+ /** The connected account the card fields act for: the charge is a direct charge on it. */
30
+ stripe_account: string | null;
31
+ }
32
+ /** A payment, as its payer sees it (`GET /public/payments/:id`). */
33
+ export interface PublicPayment extends StripeFields {
34
+ object: "payment";
35
+ id: string;
36
+ livemode: boolean;
37
+ status: PaymentStatus;
38
+ /** In the currency's smallest unit: pence, cents. */
39
+ amount: number;
40
+ currency: string;
41
+ next_action: NextAction | null;
42
+ last_error: LastError | null;
43
+ capture_method: "automatic" | "manual";
44
+ /** `off_session` when paying also saves the card. */
45
+ setup_future_usage: "off_session" | null;
46
+ /**
47
+ * What the payer can pay with here: `card` (Apple Pay and Google Pay are cards), and
48
+ * `bacs_debit` where the business takes Direct Debit and this payment allows it.
49
+ */
50
+ payment_method_types?: string[];
51
+ /**
52
+ * What Stripe's Payment Element must leave out for this payment (`bacs_debit` on a booking's
53
+ * money, a till's, a credit top-up): `elements()` passes it to Stripe.js.
54
+ */
55
+ excluded_payment_method_types?: string[];
56
+ /** How it was paid, once a method was presented: `card`, `bacs_debit`, … */
57
+ payment_method_type?: string | null;
58
+ /**
59
+ * While `processing`: when the money is expected to leave the payer's account (`YYYY-MM-DD`,
60
+ * Stripe's estimate or Tuppence's; not guaranteed). A Bacs Direct Debit is `processing` for a
61
+ * few working days — the payer has nothing more to do. `null` otherwise.
62
+ */
63
+ processing?: PaymentProcessing | null;
64
+ [field: string]: unknown;
65
+ }
66
+ /** A payment on its way: a bank debit takes days. */
67
+ export interface PaymentProcessing {
68
+ /** `YYYY-MM-DD`, or null for a method with no estimate. */
69
+ expected_debit_date: string | null;
70
+ }
71
+ /** A card being saved with nothing charged, as its customer sees it (`GET /public/setup_sessions/:id`). */
72
+ export interface PublicSetupSession extends StripeFields {
73
+ object: "setup_session";
74
+ id: string;
75
+ livemode: boolean;
76
+ status: "requires_payment_method" | "requires_action" | "processing" | "succeeded" | "canceled";
77
+ next_action: NextAction | null;
78
+ last_error: LastError | null;
79
+ [field: string]: unknown;
80
+ }
81
+ /** What `loadTuppence` learns about its key (`GET /public/sdk/config`). */
82
+ export interface SdkConfig extends StripeFields {
83
+ object: "sdk_config";
84
+ livemode: boolean;
85
+ }
86
+ /**
87
+ * A bank account as `POST /v1/accounts/{id}/external_accounts` takes it in `external_account`
88
+ * (52-09): what `bankAccountDetails` gives your page to send on, through your server, to Tuppence.
89
+ */
90
+ export interface ExternalAccountParams {
91
+ object: "bank_account";
92
+ country: string;
93
+ currency: string;
94
+ account_holder_name: string;
95
+ account_holder_type: "individual" | "company";
96
+ /** The sort code (GB) or routing number (US). None in the eurozone, where the IBAN says it. */
97
+ routing_number?: string;
98
+ /** The account number, or in the eurozone the IBAN (spaces out, upper case). */
99
+ account_number: string;
100
+ }
101
+ /** A bank account's details, for `bankAccountDetails` (41-05, 52-09, 52-17). */
102
+ export interface BankAccountDetails {
103
+ /** `GB`, `US`, or a eurozone country (`DE`, `FR`, `IE`…). */
104
+ country: string;
105
+ /** `gbp`, `usd` or `eur`. */
106
+ currency: string;
107
+ accountHolderName: string;
108
+ accountHolderType: "individual" | "company";
109
+ /** The sort code (GB, six digits) or the routing number (US). Leave it out in the eurozone. */
110
+ routingNumber?: string;
111
+ /** The account number, or in the eurozone the IBAN (spaces are fine). */
112
+ accountNumber: string;
113
+ }
114
+ /** A card top-up made on a hosted top-up page (54-18), as the person following it sees it. */
115
+ export interface PublicLinkTopup {
116
+ id: string;
117
+ /** `requires_action` (the bank's check), `processing`, `available`, `failed`… */
118
+ status: string;
119
+ amount: number;
120
+ total: number;
121
+ /** While the bank checks it's the cardholder: Stripe.js finishes it. */
122
+ client_secret: string | null;
123
+ failure_message: string | null;
124
+ [field: string]: unknown;
125
+ }
126
+ /** A hosted top-up page (54-18): `GET /public/issuing_topup_links/:token`. */
127
+ export interface PublicTopupLinkPage {
128
+ object: "issuing_topup_link_page";
129
+ livemode: boolean;
130
+ /** `open`, `used` or `expired`. */
131
+ status: string;
132
+ currency: string;
133
+ /** Fixed by the link; null: the person chooses. */
134
+ amount: number | null;
135
+ /** Where the person goes afterwards; null: nowhere — they close the page. */
136
+ return_url: string | null;
137
+ card: {
138
+ available: boolean;
139
+ total: number | null;
140
+ [field: string]: unknown;
141
+ } | null;
142
+ topup: PublicLinkTopup | null;
143
+ stripe_publishable_key: string | null;
144
+ stripe_account: string | null;
145
+ [field: string]: unknown;
146
+ }
147
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * What the browser's routes answer — the fields this SDK reads. Each object carries more (the
3
+ * business's branding, a receipt, a tip…), kept as they come.
4
+ */
5
+ export {};