@tuppence/js 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 +182 -2
- package/dist/appearance.d.ts +8 -0
- package/dist/appearance.js +25 -0
- package/dist/bank-details.d.ts +12 -0
- package/dist/bank-details.js +117 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.js +41 -0
- package/dist/express-mount.d.ts +49 -0
- package/dist/express-mount.js +272 -0
- package/dist/express.d.ts +154 -0
- package/dist/express.js +114 -0
- package/dist/http.d.ts +19 -0
- package/dist/http.js +69 -0
- package/dist/index.cjs +1304 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5 -0
- package/dist/locale.d.ts +12 -0
- package/dist/locale.js +79 -0
- package/dist/sheet.d.ts +92 -0
- package/dist/sheet.js +136 -0
- package/dist/storefront.d.ts +90 -0
- package/dist/storefront.js +25 -0
- package/dist/tuppence.d.ts +278 -0
- package/dist/tuppence.js +484 -0
- package/dist/types.d.ts +147 -0
- package/dist/types.js +5 -0
- package/package.json +43 -3
package/README.md
CHANGED
|
@@ -1,3 +1,183 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @tuppence/js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Take a card in the browser for a [Tuppence](https://tuppence.ai) payment — through Stripe's own
|
|
4
|
+
card fields, never ours. Card details go from the payer's device to Stripe, and never through
|
|
5
|
+
Tuppence, this package, or your code.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @tuppence/js
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Pay a payment
|
|
12
|
+
|
|
13
|
+
Your server creates the payment (`POST /v1/payments`) and hands the browser its `client_secret`.
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { loadTuppence } from "@tuppence/js";
|
|
17
|
+
|
|
18
|
+
// Your PUBLISHABLE key: a secret key is refused before it is sent anywhere.
|
|
19
|
+
const tuppence = await loadTuppence("pk_test_…");
|
|
20
|
+
|
|
21
|
+
// Stripe's Payment Element, in the business's colours, set up to match the charge.
|
|
22
|
+
const sheet = tuppence.paymentSheet({ clientSecret }).mount("#card");
|
|
23
|
+
sheet.on("ready", () => (payButton.disabled = false));
|
|
24
|
+
sheet.on("error", (error) => show(error.message));
|
|
25
|
+
|
|
26
|
+
form.addEventListener("submit", async (event) => {
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
const result = await sheet.confirm();
|
|
29
|
+
if (result.error) {
|
|
30
|
+
show(result.error.message); // a refused card is an answer, written for the payer
|
|
31
|
+
} else if (result.payment.status === "succeeded") {
|
|
32
|
+
done();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`confirm` presents the card (Stripe's fields make a confirmation token), lets the payer's bank
|
|
38
|
+
check it's them if it asks — in Stripe's own frame — and reads the payment back until it settles.
|
|
39
|
+
If an answer is lost on the way, it reads back what happened: a card is never presented twice.
|
|
40
|
+
A read the server briefly can't answer (a 503, no answer) is sent again, twice at most; a confirm
|
|
41
|
+
never is.
|
|
42
|
+
|
|
43
|
+
A page that adds to what is charged (a tip) calls `sheet.setAmount(total)` and passes
|
|
44
|
+
`sheet.confirm({ tipAmount })`. `wallets: "never"` hides Apple Pay and Google Pay, and
|
|
45
|
+
`layout: "accordion"` changes Stripe's layout.
|
|
46
|
+
|
|
47
|
+
A payment that waits on the payer's bank when your page opens (a charge to a saved card whose
|
|
48
|
+
bank asked) is finished with `tuppence.resumePayment({ clientSecret })`.
|
|
49
|
+
|
|
50
|
+
For your own layout, `tuppence.elements(payment)` gives Stripe's Elements group, set up to match
|
|
51
|
+
the charge, and `tuppence.confirmPayment({ clientSecret, elements })` pays with it.
|
|
52
|
+
|
|
53
|
+
## Bacs Direct Debit
|
|
54
|
+
|
|
55
|
+
Where the business takes Direct Debit, Stripe's Payment Element offers *Bacs Direct Debit* beside
|
|
56
|
+
cards, and shows the mandate and the Direct Debit Guarantee itself — don't hide or clip it. A
|
|
57
|
+
debit takes a few working days, so `confirm` answers at once with the payment `processing`:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
const { payment } = await sheet.confirm();
|
|
61
|
+
if (payment.status === "processing" && payment.payment_method_type === "bacs_debit") {
|
|
62
|
+
// "Your Direct Debit is set up. We'll collect £50.00 on or around Tue 14 Oct."
|
|
63
|
+
showSetUp(payment.amount, payment.processing.expected_debit_date); // YYYY-MM-DD, or null
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Nothing waits for `succeeded`: that (or `failed`) arrives days later, by webhook
|
|
68
|
+
(`payment.succeeded`, `payment.failed`). Stripe emails the payer to confirm the mandate.
|
|
69
|
+
Where a debit can't be used — a booking's deposit, a till, a credit top-up — the payment's
|
|
70
|
+
`excluded_payment_method_types` says so, and the sheet leaves Bacs out.
|
|
71
|
+
|
|
72
|
+
## Apple Pay, Google Pay and Link on a store
|
|
73
|
+
|
|
74
|
+
A Tuppence store's checkout puts the wallet buttons first. `expressCheckout` mounts Stripe's
|
|
75
|
+
Express Checkout Element in deferred mode (nothing is made until the shopper approves), in the
|
|
76
|
+
business's branding:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
import { createTuppence } from "@tuppence/js";
|
|
80
|
+
|
|
81
|
+
const tuppence = createTuppence();
|
|
82
|
+
const cart = tuppence.cart(); // the store's own domain; `{ base: "/<slug>" }` on the shared address
|
|
83
|
+
const basket = await cart.get();
|
|
84
|
+
|
|
85
|
+
const express = tuppence
|
|
86
|
+
.expressCheckout(checkout, { basket, onConfirm }) // `checkout`: amount, currency, Stripe's key and account
|
|
87
|
+
.mount("#express");
|
|
88
|
+
express.on("total", (quoted) => showTotal(quoted.totals.due));
|
|
89
|
+
express.on("error", (error) => show(error.message)); // "Sorry, we don't deliver to Jersey."
|
|
90
|
+
|
|
91
|
+
async function onConfirm({ confirmationToken, amount, delivery, email, shippingAddress }) {
|
|
92
|
+
// Your server pays the checkout with the token. Answer `{ error: { message } }` and the
|
|
93
|
+
// wallet says the payment failed.
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Each address and delivery choice in the wallet's sheet is priced by the basket's quote
|
|
98
|
+
(`POST /storefront/v1/cart/quote`, which changes nothing), and the sheet's total follows it
|
|
99
|
+
(`elements.update({ amount })`). Stripe gives the sheet 20 seconds to hear back; a quote that
|
|
100
|
+
takes 15 is refused (`quote_timeout`), so the sheet says so rather than hanging. An address the
|
|
101
|
+
store doesn't deliver to is refused in the sheet, and the `error` event carries the store's
|
|
102
|
+
reason. Closing the sheet puts the basket's own total back. `amount` in `onConfirm` is what the
|
|
103
|
+
sheet showed: the server charges its own figure, never the page's.
|
|
104
|
+
|
|
105
|
+
Pass `onShippingAddressChange` or `onShippingRateChange` to price the sheet yourself,
|
|
106
|
+
`shipping: false` for a basket with nothing to send, and `buttons` for Stripe's own button
|
|
107
|
+
options. On the store's own hosts the basket's cookie rides along with the quote; headless, pass
|
|
108
|
+
`tuppence.cart({ base: "https://api.tuppence.ai", publishableKey, cartToken })`.
|
|
109
|
+
|
|
110
|
+
## An address with Stripe's autocomplete
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
const elements = await tuppence.elements(checkout); // the payment's own fields
|
|
114
|
+
const address = tuppence
|
|
115
|
+
.address(elements, { mode: "shipping", defaultValues: { address: { country: "GB" } } })
|
|
116
|
+
.mount("#address");
|
|
117
|
+
address.on("change", ({ complete, address }) => complete && cart.quote({ address }));
|
|
118
|
+
const { complete, value } = await address.value(); // Stripe shows what's missing by its field
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
In the payment's own Elements group a shipping address goes with the payment, and Stripe's
|
|
122
|
+
autocomplete works with no key of yours. `tuppence.address(checkout, { mode: "billing" })` makes
|
|
123
|
+
Elements of its own, in the business's branding.
|
|
124
|
+
|
|
125
|
+
## Save a card with nothing charged
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
const sheet = tuppence.setupSheet({ clientSecret: setupClientSecret }).mount("#card");
|
|
129
|
+
const result = await sheet.confirm();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Test mode without a Stripe sandbox
|
|
133
|
+
|
|
134
|
+
An environment with no Stripe sandbox behind it runs Tuppence's simulated gateway: there are no
|
|
135
|
+
card fields (`elements()` answers `null`, and a sheet's `error` event says so), and you name one
|
|
136
|
+
of Stripe's test cards instead —
|
|
137
|
+
`confirmPayment({ clientSecret, testPaymentMethod: "pm_card_gb" })`. `tuppence.config.stripe`
|
|
138
|
+
says which gateway you are on.
|
|
139
|
+
|
|
140
|
+
## Another API address
|
|
141
|
+
|
|
142
|
+
The SDK calls `https://api.tuppence.ai`. To use another Tuppence environment you have been given,
|
|
143
|
+
pass `apiBase`, and allow that address in your page's `connect-src`:
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
const tuppence = await loadTuppence("pk_test_…", { apiBase: "https://…" });
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Your page's Content-Security-Policy
|
|
150
|
+
|
|
151
|
+
Stripe.js is loaded from `js.stripe.com` — never bundled — when card fields are first asked for.
|
|
152
|
+
Allow what Stripe's integration security guide lists, and Tuppence's API, beside your page's own
|
|
153
|
+
sources (`script-src 'self'`, and so on):
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
script-src https://js.stripe.com https://*.js.stripe.com
|
|
157
|
+
frame-src https://js.stripe.com https://*.js.stripe.com https://hooks.stripe.com
|
|
158
|
+
connect-src https://api.stripe.com https://api.tuppence.ai
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`hooks.stripe.com` is where a bank's check (3-D Secure) opens. The SDK itself needs nothing more:
|
|
162
|
+
no inline script or style, no `eval`. Tuppence's own tests run a checkout under exactly this
|
|
163
|
+
policy, read from this file, and pay through Stripe's fields.
|
|
164
|
+
|
|
165
|
+
The publishable key and client secrets travel in headers, never in a URL, and no cookie is sent
|
|
166
|
+
(except a store's own basket cookie, to its own host's basket routes).
|
|
167
|
+
|
|
168
|
+
### No integrity hash on Stripe.js
|
|
169
|
+
|
|
170
|
+
Stripe serves Stripe.js from `js.stripe.com` and changes it in place — fixes, and the fraud
|
|
171
|
+
signals Radar relies on — so it cannot carry a Subresource Integrity (SRI) hash: Stripe does not
|
|
172
|
+
support one, and does not allow Stripe.js to be bundled or hosted anywhere else. The SDK loads it
|
|
173
|
+
through Stripe's own loader (`@stripe/stripe-js`), with no `integrity` attribute. What keeps the
|
|
174
|
+
page's scripts in check is the policy above: scripts only from your origin and Stripe's.
|
|
175
|
+
|
|
176
|
+
`@tuppence/js` itself is part of your bundle, pinned by your lockfile's integrity hash.
|
|
177
|
+
|
|
178
|
+
### What this package never does
|
|
179
|
+
|
|
180
|
+
It never renders a card field of its own — Stripe's iframes take the card — and never carries
|
|
181
|
+
Stripe.js. Its build fails if either creeps in: the bundle and every published file are checked
|
|
182
|
+
for an `<input>`, for the names card fields go by (`cardnumber`, `cc-number`, `cvc`…), and for
|
|
183
|
+
Stripe.js's own code.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Appearance } from "@stripe/stripe-js";
|
|
2
|
+
import type { Branding } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Stripe's card fields dressed as the business's page (plan 09-06): its accent colour — already
|
|
5
|
+
* checked for contrast when it was set (09-08) — on Tuppence's type and corners, so the fields
|
|
6
|
+
* look like the rest of the page rather than like a form from somewhere else.
|
|
7
|
+
*/
|
|
8
|
+
export declare function appearanceFor(branding: Branding | null | undefined): Appearance;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Tuppence Blue — the pay pages' accent where the business has not set its own. */
|
|
2
|
+
const TUPPENCE_BLUE = "#3b5bff";
|
|
3
|
+
/**
|
|
4
|
+
* Stripe's card fields dressed as the business's page (plan 09-06): its accent colour — already
|
|
5
|
+
* checked for contrast when it was set (09-08) — on Tuppence's type and corners, so the fields
|
|
6
|
+
* look like the rest of the page rather than like a form from somewhere else.
|
|
7
|
+
*/
|
|
8
|
+
export function appearanceFor(branding) {
|
|
9
|
+
const accent = branding?.accent_color ?? TUPPENCE_BLUE;
|
|
10
|
+
return {
|
|
11
|
+
theme: "stripe",
|
|
12
|
+
variables: {
|
|
13
|
+
colorPrimary: accent,
|
|
14
|
+
colorText: "#1f2937",
|
|
15
|
+
colorDanger: "#b42318",
|
|
16
|
+
fontFamily: '"Inter", system-ui, -apple-system, sans-serif',
|
|
17
|
+
fontSizeBase: "16px",
|
|
18
|
+
borderRadius: "8px",
|
|
19
|
+
spacingUnit: "4px",
|
|
20
|
+
},
|
|
21
|
+
rules: {
|
|
22
|
+
".Input:focus": { borderColor: accent, boxShadow: `0 0 0 2px ${accent}33` },
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TuppenceError } from "./errors.js";
|
|
2
|
+
import type { BankAccountDetails, ExternalAccountParams } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* `bankAccountDetails`'s work (52-09): the details checked and put in the shape
|
|
5
|
+
* `POST /v1/accounts/{id}/external_accounts` takes as `external_account`. Pure — nothing is sent
|
|
6
|
+
* from here. Messages name the field, never its value.
|
|
7
|
+
*/
|
|
8
|
+
export declare function externalAccountParams(details: BankAccountDetails): {
|
|
9
|
+
externalAccount: ExternalAccountParams;
|
|
10
|
+
} | {
|
|
11
|
+
error: TuppenceError;
|
|
12
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { TuppenceError } from "./errors.js";
|
|
2
|
+
const invalid = (message, param) => new TuppenceError({ type: "invalid_request_error", code: "parameter_invalid", message, param });
|
|
3
|
+
/**
|
|
4
|
+
* The eurozone's bank accounts are IBANs (100-25): each live country's length, as Tuppence
|
|
5
|
+
* checks them. Bulgaria joins when it opens.
|
|
6
|
+
*/
|
|
7
|
+
const IBAN_LENGTHS = {
|
|
8
|
+
AT: 20,
|
|
9
|
+
BE: 16,
|
|
10
|
+
CY: 28,
|
|
11
|
+
DE: 22,
|
|
12
|
+
EE: 20,
|
|
13
|
+
ES: 24,
|
|
14
|
+
FI: 18,
|
|
15
|
+
FR: 27,
|
|
16
|
+
GR: 27,
|
|
17
|
+
HR: 21,
|
|
18
|
+
IE: 22,
|
|
19
|
+
IT: 27,
|
|
20
|
+
LT: 20,
|
|
21
|
+
LU: 20,
|
|
22
|
+
LV: 21,
|
|
23
|
+
MT: 31,
|
|
24
|
+
NL: 18,
|
|
25
|
+
PT: 25,
|
|
26
|
+
SI: 19,
|
|
27
|
+
SK: 24,
|
|
28
|
+
};
|
|
29
|
+
/** Whether an IBAN's check digits are right: its rearranged digits leave 1 when divided by 97. */
|
|
30
|
+
function mod97(iban) {
|
|
31
|
+
let remainder = 0;
|
|
32
|
+
for (const ch of `${iban.slice(4)}${iban.slice(0, 4)}`) {
|
|
33
|
+
for (const digit of String(Number.parseInt(ch, 36))) {
|
|
34
|
+
remainder = (remainder * 10 + Number(digit)) % 97;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return remainder === 1;
|
|
38
|
+
}
|
|
39
|
+
/** Why `iban` can't be a bank account in `country`, or null. Never the value itself. */
|
|
40
|
+
function ibanError(iban, country, length) {
|
|
41
|
+
if (!/^[A-Z]{2}\d{2}[A-Z0-9]+$/.test(iban) || !iban.startsWith(country)) {
|
|
42
|
+
return invalid("Enter the IBAN: it starts with the bank's country code.", "account_number");
|
|
43
|
+
}
|
|
44
|
+
if (iban.length !== length) {
|
|
45
|
+
return invalid(`Check the IBAN: it is ${length} characters in this country.`, "account_number");
|
|
46
|
+
}
|
|
47
|
+
if (!mod97(iban)) {
|
|
48
|
+
return invalid("Check the IBAN: a character is wrong.", "account_number");
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `bankAccountDetails`'s work (52-09): the details checked and put in the shape
|
|
54
|
+
* `POST /v1/accounts/{id}/external_accounts` takes as `external_account`. Pure — nothing is sent
|
|
55
|
+
* from here. Messages name the field, never its value.
|
|
56
|
+
*/
|
|
57
|
+
export function externalAccountParams(details) {
|
|
58
|
+
const country = details.country.toUpperCase();
|
|
59
|
+
const routing = (details.routingNumber ?? "").replace(/[\s-]/g, "");
|
|
60
|
+
const account = details.accountNumber.replace(/[\s-]/g, "").toUpperCase();
|
|
61
|
+
const holder = details.accountHolderName.trim();
|
|
62
|
+
const ibanLength = IBAN_LENGTHS[country];
|
|
63
|
+
if (country !== "GB" && country !== "US" && !ibanLength) {
|
|
64
|
+
return {
|
|
65
|
+
error: invalid("Bank accounts can be added in GB, the US or the eurozone.", "country"),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (!holder)
|
|
69
|
+
return { error: invalid("Enter the name on the account.", "account_holder_name") };
|
|
70
|
+
if (ibanLength) {
|
|
71
|
+
if (routing) {
|
|
72
|
+
return {
|
|
73
|
+
error: invalid("For this country, the IBAN is the whole bank account: leave the routing number out.", "routing_number"),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const error = ibanError(account, country, ibanLength);
|
|
77
|
+
if (error)
|
|
78
|
+
return { error };
|
|
79
|
+
return {
|
|
80
|
+
externalAccount: {
|
|
81
|
+
object: "bank_account",
|
|
82
|
+
country,
|
|
83
|
+
currency: details.currency.toLowerCase(),
|
|
84
|
+
account_holder_name: holder,
|
|
85
|
+
account_holder_type: details.accountHolderType,
|
|
86
|
+
account_number: account,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (country === "GB") {
|
|
91
|
+
if (!/^\d{6}$/.test(routing)) {
|
|
92
|
+
return { error: invalid("A sort code is 6 digits, like 10-88-00.", "routing_number") };
|
|
93
|
+
}
|
|
94
|
+
if (!/^\d{8}$/.test(account)) {
|
|
95
|
+
return { error: invalid("A UK account number is 8 digits.", "account_number") };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
if (!/^\d{9}$/.test(routing)) {
|
|
100
|
+
return { error: invalid("A routing number is 9 digits.", "routing_number") };
|
|
101
|
+
}
|
|
102
|
+
if (!/^\d{4,17}$/.test(account)) {
|
|
103
|
+
return { error: invalid("Check the account number: 4 to 17 digits.", "account_number") };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
externalAccount: {
|
|
108
|
+
object: "bank_account",
|
|
109
|
+
country,
|
|
110
|
+
currency: details.currency.toLowerCase(),
|
|
111
|
+
account_holder_name: holder,
|
|
112
|
+
account_holder_type: details.accountHolderType,
|
|
113
|
+
routing_number: routing,
|
|
114
|
+
account_number: account,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why something did not happen — Tuppence's error envelope, or Stripe.js's, in one shape.
|
|
3
|
+
*
|
|
4
|
+
* `type` is `card_error` for a card the bank refused (show `message` to the payer: it is written
|
|
5
|
+
* for them), `validation_error` for card fields that are not complete, `api_connection_error`
|
|
6
|
+
* when Tuppence could not be reached, and otherwise the API's own type.
|
|
7
|
+
*/
|
|
8
|
+
export declare class TuppenceError extends Error {
|
|
9
|
+
readonly type: string;
|
|
10
|
+
readonly code: string | null;
|
|
11
|
+
readonly declineCode: string | null;
|
|
12
|
+
readonly param: string | null;
|
|
13
|
+
/** The HTTP status, or 0 when there was no answer. */
|
|
14
|
+
readonly status: number;
|
|
15
|
+
constructor(fields: {
|
|
16
|
+
type: string;
|
|
17
|
+
message: string;
|
|
18
|
+
code?: string | null;
|
|
19
|
+
declineCode?: string | null;
|
|
20
|
+
param?: string | null;
|
|
21
|
+
status?: number;
|
|
22
|
+
});
|
|
23
|
+
/** The API's envelope: `{ error: { type, code, message, param, decline_code } }`. */
|
|
24
|
+
static fromEnvelope(status: number, body: unknown): TuppenceError;
|
|
25
|
+
/** Stripe.js's error (card fields, a confirmation token, a bank's check). */
|
|
26
|
+
static fromStripe(error: {
|
|
27
|
+
type?: string;
|
|
28
|
+
message?: string;
|
|
29
|
+
code?: string;
|
|
30
|
+
decline_code?: string;
|
|
31
|
+
param?: string;
|
|
32
|
+
}): TuppenceError;
|
|
33
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why something did not happen — Tuppence's error envelope, or Stripe.js's, in one shape.
|
|
3
|
+
*
|
|
4
|
+
* `type` is `card_error` for a card the bank refused (show `message` to the payer: it is written
|
|
5
|
+
* for them), `validation_error` for card fields that are not complete, `api_connection_error`
|
|
6
|
+
* when Tuppence could not be reached, and otherwise the API's own type.
|
|
7
|
+
*/
|
|
8
|
+
export class TuppenceError extends Error {
|
|
9
|
+
constructor(fields) {
|
|
10
|
+
super(fields.message);
|
|
11
|
+
this.name = "TuppenceError";
|
|
12
|
+
this.type = fields.type;
|
|
13
|
+
this.code = fields.code ?? null;
|
|
14
|
+
this.declineCode = fields.declineCode ?? null;
|
|
15
|
+
this.param = fields.param ?? null;
|
|
16
|
+
this.status = fields.status ?? 0;
|
|
17
|
+
}
|
|
18
|
+
/** The API's envelope: `{ error: { type, code, message, param, decline_code } }`. */
|
|
19
|
+
static fromEnvelope(status, body) {
|
|
20
|
+
const error = body?.error ?? {};
|
|
21
|
+
const text = (key) => (typeof error[key] === "string" ? String(error[key]) : null);
|
|
22
|
+
return new TuppenceError({
|
|
23
|
+
type: text("type") ?? "api_error",
|
|
24
|
+
message: text("message") ?? `Tuppence answered ${status}.`,
|
|
25
|
+
code: text("code"),
|
|
26
|
+
declineCode: text("decline_code"),
|
|
27
|
+
param: text("param"),
|
|
28
|
+
status,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** Stripe.js's error (card fields, a confirmation token, a bank's check). */
|
|
32
|
+
static fromStripe(error) {
|
|
33
|
+
return new TuppenceError({
|
|
34
|
+
type: error.type ?? "api_error",
|
|
35
|
+
message: error.message ?? "Stripe could not take the card.",
|
|
36
|
+
code: error.code ?? null,
|
|
37
|
+
declineCode: error.decline_code ?? null,
|
|
38
|
+
param: error.param ?? null,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { StripeElements } from "@stripe/stripe-js";
|
|
2
|
+
import { type AddressHandlers, type AddressOptions, type AddressValue, type ExpressCheckoutOptions, type ExpressHandlers, type Host } from "./express.js";
|
|
3
|
+
import type { CartAddress, StorefrontCartClient } from "./storefront.js";
|
|
4
|
+
import type { Payable, Tuppence } from "./tuppence.js";
|
|
5
|
+
/**
|
|
6
|
+
* A wallet's or the Address Element's address, as the quote takes it. Wallets redact it until
|
|
7
|
+
* the payment is authorised (a city, a region, part of a postcode). A region that is a name
|
|
8
|
+
* ("West Yorkshire") is no code, so it is left out.
|
|
9
|
+
*/
|
|
10
|
+
export declare function cartAddressOf(address: {
|
|
11
|
+
country?: string | null;
|
|
12
|
+
state?: string | null;
|
|
13
|
+
postal_code?: string | null;
|
|
14
|
+
}): CartAddress;
|
|
15
|
+
/** The address as the basket's quote takes it, once there is a country. */
|
|
16
|
+
export declare const asCartAddress: (value: AddressValue | undefined) => CartAddress | null;
|
|
17
|
+
/**
|
|
18
|
+
* What `ExpressCheckout` does once it is mounted (85-10): Stripe's Express Checkout Element (Apple
|
|
19
|
+
* Pay, Google Pay, Link) for a store's basket, in deferred mode: nothing is made until the shopper
|
|
20
|
+
* approves. Each address and delivery choice in the sheet is priced by the basket's quote and the
|
|
21
|
+
* sheet's total follows it (`elements.update({ amount })`); the server charges its own figure at
|
|
22
|
+
* the end. One per `ExpressCheckout`, so a remount keeps the basket as last quoted.
|
|
23
|
+
*/
|
|
24
|
+
export declare class Wallet {
|
|
25
|
+
private readonly host;
|
|
26
|
+
private readonly tuppence;
|
|
27
|
+
private readonly payable;
|
|
28
|
+
private readonly options;
|
|
29
|
+
private readonly cart;
|
|
30
|
+
private basket;
|
|
31
|
+
private address;
|
|
32
|
+
private delivery;
|
|
33
|
+
private amount;
|
|
34
|
+
private elements;
|
|
35
|
+
constructor(host: Host<ExpressHandlers>, tuppence: Tuppence, payable: Payable, options: ExpressCheckoutOptions, cart: StorefrontCartClient);
|
|
36
|
+
/** The delivery choices as the sheet lists them: the chosen one first, as Stripe selects it. */
|
|
37
|
+
private ratesOf;
|
|
38
|
+
/** A quote, or why there is none: refused after 15 s rather than left to hang the sheet. */
|
|
39
|
+
private within;
|
|
40
|
+
/** The quote's answer for the sheet, or the reason it can't be delivered. */
|
|
41
|
+
private deliverable;
|
|
42
|
+
private priced;
|
|
43
|
+
private addressChanged;
|
|
44
|
+
private rateChanged;
|
|
45
|
+
private confirmed;
|
|
46
|
+
load(target: string | HTMLElement): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/** Stripe's Address Element, with Stripe's autocomplete, for `AddressField`. */
|
|
49
|
+
export declare function mountAddress(host: Host<AddressHandlers>, tuppence: Tuppence, from: StripeElements | Payable, options: AddressOptions, target: string | HTMLElement): Promise<void>;
|