@flopay/shared 1.3.1 → 1.3.3
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 +25 -10
- package/dist/index.cjs +126 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +99 -13
- package/dist/index.d.ts +99 -13
- package/dist/index.mjs +121 -33
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -178,25 +178,40 @@ throw validationError('Email is required', 'email');
|
|
|
178
178
|
|
|
179
179
|
### Postal Code Helpers
|
|
180
180
|
|
|
181
|
-
Country-aware postcode format validation,
|
|
181
|
+
Country-aware postcode format validation, a **faithful mirror of the billing API's own validator** — `is-valid-postal-code.validator.ts` (TeamFloPay/backend#895), same pinned [`validator`](https://www.npmjs.com/package/validator)`@13.15.35` `isPostalCode`, same country→ISO-2 normalization, same fail-open rules — so the SDK never blocks a postcode the server accepts nor opens its submit gate on one the server's `PATCH …/account` will reject with a 400. `@flopay/react`'s AVS block uses these to block a malformed postcode before the card is captured and to show the expected format inline.
|
|
182
182
|
|
|
183
183
|
| Export | Description |
|
|
184
184
|
|--------|-------------|
|
|
185
185
|
| `getPostalCodeLabel(countryCode)` | Country-appropriate field label (`'ZIP Code'`, `'Postcode'`, `'Eircode'`, …); defaults to `'Postal Code'` |
|
|
186
|
-
| `isPostalCodeSupported(country)` | `true` when `validator` has a postcode pattern for the
|
|
187
|
-
| `isValidPostalCode(country, postalCode)` | `true` when the (trimmed) postcode matches the country's format. **Fails open**
|
|
188
|
-
| `getPostalCodeExample(country)` |
|
|
186
|
+
| `isPostalCodeSupported(country)` | `true` when `validator` has a postcode pattern for the country. Normalization matches the backend: a **2-letter code is passed straight through** (so `'UK'`, not a `validator` locale, → `false`); non-2-letter names resolve via the backend's `COUNTRY_ALIASES` (`'United Kingdom'` → `GB`). Unresolvable / no-postcode countries return `false` |
|
|
187
|
+
| `isValidPostalCode(country, postalCode)` | `true` when the (trimmed) postcode matches the country's format. **Fails open** in exactly the backend's three cases: blank/whitespace postcode, unresolvable country, or an unsupported `validator` locale. Callers distinguish "required" (empty) from "malformed" by checking emptiness + `isPostalCodeSupported` themselves — an empty value fails open here, as it does server-side |
|
|
188
|
+
| `getPostalCodeExample(country)` | The example postcode the backend embeds in its 400 message (US `12345 or 12345-6789`, GB `SW1A 1AA`, CA `A1A 1A1`, …), or `undefined` when there is none. Kept verbatim in sync with the backend's `POSTAL_CODE_EXAMPLES` so the SDK's inline hint and the server's rejection message never contradict |
|
|
189
189
|
| `getStateFromPostalCode(country, postalCode)` | Derives a US state / CA province code from a postcode for AVS enrichment; `null` for other countries or a malformed code |
|
|
190
190
|
|
|
191
|
+
### Idempotency Helpers
|
|
192
|
+
|
|
193
|
+
Primitives for the stable checkout-create `Idempotency-Key` (TeamFloPay/backend#972). `@flopay/js` and `@flopay/react` use these automatically; import them only if you build your own create flow.
|
|
194
|
+
|
|
195
|
+
| Export | Description |
|
|
196
|
+
|--------|-------------|
|
|
197
|
+
| `IDEMPOTENCY_KEY_HEADER` | The header name (`'Idempotency-Key'`) the SDK sends on `POST /v1/checkouts/sessions`. Optional on the backend — absent means the legacy, non-idempotent path. |
|
|
198
|
+
| `MAX_IDEMPOTENCY_KEY_LENGTH` | `255` — the documented max length a supplied key may have. |
|
|
199
|
+
| `IDEMPOTENCY_IN_PROGRESS_CODE` | Backend `code` (`'IdempotencyKeyInProgress'`) marking a **retryable** in-progress replay: retry with the *same* key. Distinct from a payload-conflict (`409`), which is non-retryable. |
|
|
200
|
+
| `generateIdempotencyKey()` | Returns a cryptographically random, high-entropy key (`crypto.randomUUID()`, else 16 random bytes). Returns `undefined` when no secure RNG exists, so callers omit the header rather than emit a weak key. Never derived from request data. |
|
|
201
|
+
| `resolveIdempotencyKey(supplied?)` | Returns a supplied key unchanged after validating it is non-empty and ≤ `MAX_IDEMPOTENCY_KEY_LENGTH` (throws `FloPayError('validation_error')` otherwise, never echoing the value), or a freshly generated key when none is supplied. |
|
|
202
|
+
|
|
191
203
|
```ts
|
|
192
204
|
import { isPostalCodeSupported, isValidPostalCode, getPostalCodeExample } from '@flopay/shared';
|
|
193
205
|
|
|
194
|
-
isPostalCodeSupported('US');
|
|
195
|
-
isPostalCodeSupported('AE');
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
isValidPostalCode('
|
|
199
|
-
|
|
206
|
+
isPostalCodeSupported('US'); // true
|
|
207
|
+
isPostalCodeSupported('AE'); // false — UAE has no postcodes
|
|
208
|
+
isPostalCodeSupported('United Kingdom'); // true — full name → GB
|
|
209
|
+
isPostalCodeSupported('UK'); // false — 2-letter passthrough (not a validator locale)
|
|
210
|
+
isValidPostalCode('GB', 'SW1A 1AA'); // true
|
|
211
|
+
isValidPostalCode('GB', '12345'); // false — US shape in GB
|
|
212
|
+
isValidPostalCode('AE', ''); // true — fail open, never blocks
|
|
213
|
+
isValidPostalCode('US', ''); // true — blank fails open (enforce "required" separately)
|
|
214
|
+
getPostalCodeExample('CA'); // 'A1A 1A1'
|
|
200
215
|
```
|
|
201
216
|
|
|
202
217
|
> **Runtime dependency.** Unlike the rest of `@flopay/shared`, these helpers pull in `validator` (imported via the `validator/lib/isPostalCode.js` submodule so bundlers only include the one check). It installs transitively when you depend on `@flopay/shared`.
|
package/dist/index.cjs
CHANGED
|
@@ -58,6 +58,9 @@ __export(index_exports, {
|
|
|
58
58
|
FloPayError: () => FloPayError,
|
|
59
59
|
GLASS_DARK_APPEARANCE: () => GLASS_DARK_APPEARANCE,
|
|
60
60
|
GLASS_LIGHT_APPEARANCE: () => GLASS_LIGHT_APPEARANCE,
|
|
61
|
+
IDEMPOTENCY_IN_PROGRESS_CODE: () => IDEMPOTENCY_IN_PROGRESS_CODE,
|
|
62
|
+
IDEMPOTENCY_KEY_HEADER: () => IDEMPOTENCY_KEY_HEADER,
|
|
63
|
+
MAX_IDEMPOTENCY_KEY_LENGTH: () => MAX_IDEMPOTENCY_KEY_LENGTH,
|
|
61
64
|
MODERN_DARK_APPEARANCE: () => MODERN_DARK_APPEARANCE,
|
|
62
65
|
MODERN_LIGHT_APPEARANCE: () => MODERN_LIGHT_APPEARANCE,
|
|
63
66
|
NIGHT_APPEARANCE: () => NIGHT_APPEARANCE,
|
|
@@ -81,6 +84,7 @@ __export(index_exports, {
|
|
|
81
84
|
filterStripeMethodsByCountry: () => filterStripeMethodsByCountry,
|
|
82
85
|
filterStripeMethodsByCurrency: () => filterStripeMethodsByCurrency,
|
|
83
86
|
foldIntoProducts: () => foldIntoProducts,
|
|
87
|
+
generateIdempotencyKey: () => generateIdempotencyKey,
|
|
84
88
|
getConfiguredBillingApiUrl: () => getConfiguredBillingApiUrl,
|
|
85
89
|
getCountryByCode: () => getCountryByCode,
|
|
86
90
|
getCurrencyByCountry: () => getCurrencyByCountry,
|
|
@@ -107,6 +111,7 @@ __export(index_exports, {
|
|
|
107
111
|
resolveAVSConfig: () => resolveAVSConfig,
|
|
108
112
|
resolveBillingApiUrl: () => resolveBillingApiUrl,
|
|
109
113
|
resolveButtonsLayoutTheme: () => resolveButtonsLayoutTheme,
|
|
114
|
+
resolveIdempotencyKey: () => resolveIdempotencyKey,
|
|
110
115
|
resolveSessionCurrency: () => resolveSessionCurrency,
|
|
111
116
|
resolveStripeMethodBrandVariant: () => resolveStripeMethodBrandVariant,
|
|
112
117
|
resolveTheme: () => resolveTheme,
|
|
@@ -196,7 +201,7 @@ var PAYMENT_METHOD_LOGOS = {
|
|
|
196
201
|
};
|
|
197
202
|
|
|
198
203
|
// src/constants.ts
|
|
199
|
-
var SDK_VERSION = "1.3.
|
|
204
|
+
var SDK_VERSION = "1.3.3";
|
|
200
205
|
var FLO_SDK_VERSION_HEADER = "x-flo-sdk-version";
|
|
201
206
|
var BILLING_API_URL_STAGING = "https://api.stage.flopay.com";
|
|
202
207
|
var BILLING_API_URL_PRODUCTION = "https://api.flopay.com";
|
|
@@ -1985,53 +1990,93 @@ if (typeof isPostalCode !== "function") {
|
|
|
1985
1990
|
var SUPPORTED_LOCALES = new Set(
|
|
1986
1991
|
mod.locales.map((code) => code.toUpperCase())
|
|
1987
1992
|
);
|
|
1988
|
-
var
|
|
1989
|
-
|
|
1993
|
+
var COUNTRY_ALIASES = {
|
|
1994
|
+
AUSTRALIA: "AU",
|
|
1995
|
+
BRAZIL: "BR",
|
|
1996
|
+
CANADA: "CA",
|
|
1997
|
+
CH: "CH",
|
|
1998
|
+
DENMARK: "DK",
|
|
1999
|
+
DE: "DE",
|
|
2000
|
+
DEUTSCHLAND: "DE",
|
|
2001
|
+
FRANCE: "FR",
|
|
2002
|
+
GB: "GB",
|
|
2003
|
+
GERMANY: "DE",
|
|
2004
|
+
GREATBRITAIN: "GB",
|
|
2005
|
+
GREAT_BRITAIN: "GB",
|
|
2006
|
+
INDIA: "IN",
|
|
2007
|
+
IRELAND: "IE",
|
|
2008
|
+
ITALY: "IT",
|
|
2009
|
+
JAPAN: "JP",
|
|
2010
|
+
MEXICO: "MX",
|
|
2011
|
+
NETHERLANDS: "NL",
|
|
2012
|
+
NEWZEALAND: "NZ",
|
|
2013
|
+
NORWAY: "NO",
|
|
2014
|
+
POLAND: "PL",
|
|
2015
|
+
PORTUGAL: "PT",
|
|
2016
|
+
SINGAPORE: "SG",
|
|
2017
|
+
SOUTHAFRICA: "ZA",
|
|
2018
|
+
SPAIN: "ES",
|
|
2019
|
+
SWEDEN: "SE",
|
|
2020
|
+
SWITZERLAND: "CH",
|
|
2021
|
+
UK: "GB",
|
|
2022
|
+
UNITEDKINGDOM: "GB",
|
|
2023
|
+
UNITEDSTATES: "US",
|
|
2024
|
+
US: "US",
|
|
2025
|
+
USA: "US",
|
|
2026
|
+
"UNITED STATES": "US",
|
|
2027
|
+
"UNITED KINGDOM": "GB",
|
|
2028
|
+
"GREAT BRITAIN": "GB",
|
|
2029
|
+
ENGLAND: "GB"
|
|
1990
2030
|
};
|
|
1991
|
-
function
|
|
1992
|
-
const upper = country.trim().toUpperCase();
|
|
1993
|
-
|
|
2031
|
+
function normalizeCountryToIso2(country) {
|
|
2032
|
+
const upper = (country ?? "").trim().toUpperCase();
|
|
2033
|
+
if (!upper) return null;
|
|
2034
|
+
if (upper.length === 2) return upper;
|
|
2035
|
+
return COUNTRY_ALIASES[upper] ?? null;
|
|
1994
2036
|
}
|
|
1995
2037
|
var POSTAL_CODE_EXAMPLES = {
|
|
1996
|
-
US: "12345 or 12345-6789",
|
|
1997
|
-
GB: "SW1A 1AA",
|
|
1998
|
-
CA: "A1A 1A1",
|
|
1999
2038
|
AU: "2000",
|
|
2000
|
-
|
|
2001
|
-
|
|
2039
|
+
BR: "01000-000",
|
|
2040
|
+
CA: "A1A 1A1",
|
|
2041
|
+
CH: "8001",
|
|
2002
2042
|
DE: "10115",
|
|
2003
|
-
|
|
2004
|
-
NL: "1011 AB",
|
|
2043
|
+
DK: "1050",
|
|
2005
2044
|
ES: "28001",
|
|
2006
|
-
|
|
2045
|
+
FR: "75008",
|
|
2046
|
+
GB: "SW1A 1AA",
|
|
2047
|
+
IE: "D02 X285",
|
|
2007
2048
|
IN: "110001",
|
|
2049
|
+
IT: "00118",
|
|
2008
2050
|
JP: "100-0001",
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2051
|
+
MX: "06500",
|
|
2052
|
+
NL: "1012 JS",
|
|
2053
|
+
NO: "0150",
|
|
2054
|
+
NZ: "6011",
|
|
2013
2055
|
PL: "00-001",
|
|
2014
|
-
PT: "
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
FI: "00100",
|
|
2020
|
-
AT: "1010",
|
|
2021
|
-
BE: "1000",
|
|
2022
|
-
CZ: "100 00"
|
|
2056
|
+
PT: "1100-148",
|
|
2057
|
+
SE: "111 22",
|
|
2058
|
+
SG: "018989",
|
|
2059
|
+
US: "12345 or 12345-6789",
|
|
2060
|
+
ZA: "8001"
|
|
2023
2061
|
};
|
|
2024
2062
|
function isPostalCodeSupported(country) {
|
|
2025
|
-
|
|
2026
|
-
return SUPPORTED_LOCALES.has(
|
|
2063
|
+
const iso2 = normalizeCountryToIso2(country);
|
|
2064
|
+
return iso2 !== null && SUPPORTED_LOCALES.has(iso2);
|
|
2027
2065
|
}
|
|
2028
2066
|
function isValidPostalCode(country, postalCode) {
|
|
2029
|
-
|
|
2030
|
-
|
|
2067
|
+
const trimmed = (postalCode ?? "").trim();
|
|
2068
|
+
if (!trimmed) return true;
|
|
2069
|
+
const iso2 = normalizeCountryToIso2(country);
|
|
2070
|
+
if (iso2 === null || !SUPPORTED_LOCALES.has(iso2)) return true;
|
|
2071
|
+
try {
|
|
2072
|
+
return isPostalCode(trimmed, iso2);
|
|
2073
|
+
} catch {
|
|
2074
|
+
return true;
|
|
2075
|
+
}
|
|
2031
2076
|
}
|
|
2032
2077
|
function getPostalCodeExample(country) {
|
|
2033
|
-
|
|
2034
|
-
return POSTAL_CODE_EXAMPLES[
|
|
2078
|
+
const iso2 = normalizeCountryToIso2(country);
|
|
2079
|
+
return iso2 ? POSTAL_CODE_EXAMPLES[iso2] : void 0;
|
|
2035
2080
|
}
|
|
2036
2081
|
|
|
2037
2082
|
// src/checkout-payload.ts
|
|
@@ -2234,6 +2279,49 @@ function isValidSecretKey(key) {
|
|
|
2234
2279
|
function isSetupIntentClientSecret(clientSecret) {
|
|
2235
2280
|
return typeof clientSecret === "string" && clientSecret.startsWith("seti_");
|
|
2236
2281
|
}
|
|
2282
|
+
|
|
2283
|
+
// src/idempotency.ts
|
|
2284
|
+
var IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
|
|
2285
|
+
var MAX_IDEMPOTENCY_KEY_LENGTH = 255;
|
|
2286
|
+
var IDEMPOTENCY_IN_PROGRESS_CODE = "IdempotencyKeyInProgress";
|
|
2287
|
+
function getSecureRandomSource() {
|
|
2288
|
+
const source = globalThis.crypto;
|
|
2289
|
+
if (!source) return void 0;
|
|
2290
|
+
if (typeof source.randomUUID === "function" || typeof source.getRandomValues === "function") {
|
|
2291
|
+
return source;
|
|
2292
|
+
}
|
|
2293
|
+
return void 0;
|
|
2294
|
+
}
|
|
2295
|
+
function generateIdempotencyKey() {
|
|
2296
|
+
const source = getSecureRandomSource();
|
|
2297
|
+
if (!source) return void 0;
|
|
2298
|
+
if (typeof source.randomUUID === "function") {
|
|
2299
|
+
return source.randomUUID();
|
|
2300
|
+
}
|
|
2301
|
+
const bytes = new Uint8Array(16);
|
|
2302
|
+
source.getRandomValues(bytes);
|
|
2303
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2304
|
+
}
|
|
2305
|
+
function resolveIdempotencyKey(supplied) {
|
|
2306
|
+
if (supplied === void 0) {
|
|
2307
|
+
return generateIdempotencyKey();
|
|
2308
|
+
}
|
|
2309
|
+
if (typeof supplied !== "string" || supplied.trim() === "") {
|
|
2310
|
+
throw new FloPayError(
|
|
2311
|
+
"idempotencyKey must be a non-empty string identifying exactly one checkout creation.",
|
|
2312
|
+
"validation_error",
|
|
2313
|
+
{ code: "InvalidIdempotencyKey", param: "idempotencyKey" }
|
|
2314
|
+
);
|
|
2315
|
+
}
|
|
2316
|
+
if (supplied.length > MAX_IDEMPOTENCY_KEY_LENGTH) {
|
|
2317
|
+
throw new FloPayError(
|
|
2318
|
+
`idempotencyKey must be at most ${MAX_IDEMPOTENCY_KEY_LENGTH} characters.`,
|
|
2319
|
+
"validation_error",
|
|
2320
|
+
{ code: "InvalidIdempotencyKey", param: "idempotencyKey" }
|
|
2321
|
+
);
|
|
2322
|
+
}
|
|
2323
|
+
return supplied;
|
|
2324
|
+
}
|
|
2237
2325
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2238
2326
|
0 && (module.exports = {
|
|
2239
2327
|
BILLING_API_URL,
|
|
@@ -2264,6 +2352,9 @@ function isSetupIntentClientSecret(clientSecret) {
|
|
|
2264
2352
|
FloPayError,
|
|
2265
2353
|
GLASS_DARK_APPEARANCE,
|
|
2266
2354
|
GLASS_LIGHT_APPEARANCE,
|
|
2355
|
+
IDEMPOTENCY_IN_PROGRESS_CODE,
|
|
2356
|
+
IDEMPOTENCY_KEY_HEADER,
|
|
2357
|
+
MAX_IDEMPOTENCY_KEY_LENGTH,
|
|
2267
2358
|
MODERN_DARK_APPEARANCE,
|
|
2268
2359
|
MODERN_LIGHT_APPEARANCE,
|
|
2269
2360
|
NIGHT_APPEARANCE,
|
|
@@ -2287,6 +2378,7 @@ function isSetupIntentClientSecret(clientSecret) {
|
|
|
2287
2378
|
filterStripeMethodsByCountry,
|
|
2288
2379
|
filterStripeMethodsByCurrency,
|
|
2289
2380
|
foldIntoProducts,
|
|
2381
|
+
generateIdempotencyKey,
|
|
2290
2382
|
getConfiguredBillingApiUrl,
|
|
2291
2383
|
getCountryByCode,
|
|
2292
2384
|
getCurrencyByCountry,
|
|
@@ -2313,6 +2405,7 @@ function isSetupIntentClientSecret(clientSecret) {
|
|
|
2313
2405
|
resolveAVSConfig,
|
|
2314
2406
|
resolveBillingApiUrl,
|
|
2315
2407
|
resolveButtonsLayoutTheme,
|
|
2408
|
+
resolveIdempotencyKey,
|
|
2316
2409
|
resolveSessionCurrency,
|
|
2317
2410
|
resolveStripeMethodBrandVariant,
|
|
2318
2411
|
resolveTheme,
|