@flopay/js 1.3.2 → 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 +39 -0
- package/dist/index.cjs +51 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.mjs +57 -26
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -695,6 +695,13 @@ declare function createCheckoutSession(options: CreateSessionParams): Promise<Ch
|
|
|
695
695
|
* Creates a checkout session with automatic retry on timeout/abort errors.
|
|
696
696
|
*
|
|
697
697
|
* Uses exponential backoff: 100ms, 200ms, 400ms, etc.
|
|
698
|
+
*
|
|
699
|
+
* The idempotency key (supplied or generated) is resolved **once**, before the
|
|
700
|
+
* retry loop, and reused for every attempt — so a timeout, a lost response, or
|
|
701
|
+
* a documented in-progress reply all replay the *same* key and cannot mint a
|
|
702
|
+
* second checkout session (TeamFloPay/backend#972). A later independent call
|
|
703
|
+
* resolves its own fresh key. A payload-conflict (`409`) is surfaced without
|
|
704
|
+
* retrying, since only the exact same request may safely replay a key.
|
|
698
705
|
*/
|
|
699
706
|
declare function createCheckoutSessionWithRetries(options: CreateSessionParams & {
|
|
700
707
|
maxRetries?: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -695,6 +695,13 @@ declare function createCheckoutSession(options: CreateSessionParams): Promise<Ch
|
|
|
695
695
|
* Creates a checkout session with automatic retry on timeout/abort errors.
|
|
696
696
|
*
|
|
697
697
|
* Uses exponential backoff: 100ms, 200ms, 400ms, etc.
|
|
698
|
+
*
|
|
699
|
+
* The idempotency key (supplied or generated) is resolved **once**, before the
|
|
700
|
+
* retry loop, and reused for every attempt — so a timeout, a lost response, or
|
|
701
|
+
* a documented in-progress reply all replay the *same* key and cannot mint a
|
|
702
|
+
* second checkout session (TeamFloPay/backend#972). A later independent call
|
|
703
|
+
* resolves its own fresh key. A payload-conflict (`409`) is surfaced without
|
|
704
|
+
* retrying, since only the exact same request may safely replay a key.
|
|
698
705
|
*/
|
|
699
706
|
declare function createCheckoutSessionWithRetries(options: CreateSessionParams & {
|
|
700
707
|
maxRetries?: number;
|
package/dist/index.mjs
CHANGED
|
@@ -517,8 +517,11 @@ import {
|
|
|
517
517
|
FloPayError as FloPayError3,
|
|
518
518
|
SDK_VERSION,
|
|
519
519
|
FLO_SDK_VERSION_HEADER,
|
|
520
|
+
IDEMPOTENCY_KEY_HEADER,
|
|
521
|
+
IDEMPOTENCY_IN_PROGRESS_CODE,
|
|
520
522
|
buildProductPayload,
|
|
521
523
|
foldIntoProducts,
|
|
524
|
+
resolveIdempotencyKey,
|
|
522
525
|
resolveSessionCurrency
|
|
523
526
|
} from "@flopay/shared";
|
|
524
527
|
|
|
@@ -635,6 +638,7 @@ async function buildApiErrorFromResponse(response, fallbackMessage) {
|
|
|
635
638
|
});
|
|
636
639
|
}
|
|
637
640
|
var NETWORK_RETRY_ATTEMPTS = 2;
|
|
641
|
+
var IDEMPOTENCY_IN_PROGRESS_RETRY_ATTEMPTS = 2;
|
|
638
642
|
async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEMPTS) {
|
|
639
643
|
let lastErr;
|
|
640
644
|
for (let attempt = 0; ; attempt++) {
|
|
@@ -981,29 +985,41 @@ var PaymentAPI = class {
|
|
|
981
985
|
if (params.checkoutType) payload["checkoutType"] = params.checkoutType;
|
|
982
986
|
if (params.checkoutLayout) payload["checkoutLayout"] = params.checkoutLayout;
|
|
983
987
|
if (params.avsConfig) payload["avsConfig"] = params.avsConfig;
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
},
|
|
995
|
-
body: JSON.stringify(payload)
|
|
996
|
-
}
|
|
997
|
-
);
|
|
998
|
-
if (response.status === 204) {
|
|
999
|
-
throw new FloPayError3(
|
|
1000
|
-
"Session auto-completed \u2014 payment method already on file",
|
|
1001
|
-
"api_error",
|
|
1002
|
-
{ code: "session_auto_completed" }
|
|
1003
|
-
);
|
|
988
|
+
const headers = {
|
|
989
|
+
"Content-Type": "application/json",
|
|
990
|
+
// Declare the SDK version so backends at TeamFloPay/backend#823 embed
|
|
991
|
+
// the hosted vault capture block (`body.vault`) in the response for
|
|
992
|
+
// SDKs ≥ 1.3.0. Older backends ignore the header.
|
|
993
|
+
[FLO_SDK_VERSION_HEADER]: SDK_VERSION
|
|
994
|
+
};
|
|
995
|
+
const idempotencyKey = resolveIdempotencyKey(params.idempotencyKey);
|
|
996
|
+
if (idempotencyKey) {
|
|
997
|
+
headers[IDEMPOTENCY_KEY_HEADER] = idempotencyKey;
|
|
1004
998
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
999
|
+
let response;
|
|
1000
|
+
for (let attempt = 0; ; attempt++) {
|
|
1001
|
+
response = await fetchWithNetworkRetry(
|
|
1002
|
+
`${this.baseUrl}/v1/checkouts/sessions?expand=true`,
|
|
1003
|
+
{
|
|
1004
|
+
method: "POST",
|
|
1005
|
+
headers,
|
|
1006
|
+
body: JSON.stringify(payload)
|
|
1007
|
+
}
|
|
1008
|
+
);
|
|
1009
|
+
if (response.status === 204) {
|
|
1010
|
+
throw new FloPayError3(
|
|
1011
|
+
"Session auto-completed \u2014 payment method already on file",
|
|
1012
|
+
"api_error",
|
|
1013
|
+
{ code: "session_auto_completed" }
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
if (response.ok) break;
|
|
1017
|
+
const error = await buildApiErrorFromResponse(response, "Failed to create checkout session");
|
|
1018
|
+
if (error.code === IDEMPOTENCY_IN_PROGRESS_CODE && attempt < IDEMPOTENCY_IN_PROGRESS_RETRY_ATTEMPTS) {
|
|
1019
|
+
await delay(150 * 2 ** attempt);
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
throw error;
|
|
1007
1023
|
}
|
|
1008
1024
|
const body = await response.json();
|
|
1009
1025
|
if (body.data && "gateways" in body.data) {
|
|
@@ -1802,9 +1818,12 @@ async function loadFloPay(publishableKey, options) {
|
|
|
1802
1818
|
// src/create-checkout-session.ts
|
|
1803
1819
|
import {
|
|
1804
1820
|
FloPayError as FloPayError7,
|
|
1821
|
+
IDEMPOTENCY_IN_PROGRESS_CODE as IDEMPOTENCY_IN_PROGRESS_CODE2,
|
|
1822
|
+
IDEMPOTENCY_KEY_HEADER as IDEMPOTENCY_KEY_HEADER2,
|
|
1805
1823
|
SDK_VERSION as SDK_VERSION2,
|
|
1806
1824
|
buildProductPayload as buildProductPayload2,
|
|
1807
1825
|
foldIntoProducts as foldIntoProducts2,
|
|
1826
|
+
resolveIdempotencyKey as resolveIdempotencyKey2,
|
|
1808
1827
|
resolveSessionCurrency as resolveSessionCurrency2
|
|
1809
1828
|
} from "@flopay/shared";
|
|
1810
1829
|
var MAX_COUPON_CODES = 5;
|
|
@@ -1845,8 +1864,10 @@ async function createCheckoutSession(options) {
|
|
|
1845
1864
|
timeoutMs = 12e3,
|
|
1846
1865
|
clientId,
|
|
1847
1866
|
currency,
|
|
1848
|
-
utmMetadata
|
|
1867
|
+
utmMetadata,
|
|
1868
|
+
idempotencyKey
|
|
1849
1869
|
} = options;
|
|
1870
|
+
const resolvedIdempotencyKey = resolveIdempotencyKey2(idempotencyKey);
|
|
1850
1871
|
if (couponCodes.length > MAX_COUPON_CODES) {
|
|
1851
1872
|
throw new FloPayError7(
|
|
1852
1873
|
`Too many coupon codes \u2014 a checkout session accepts at most ${MAX_COUPON_CODES}.`,
|
|
@@ -1895,12 +1916,16 @@ async function createCheckoutSession(options) {
|
|
|
1895
1916
|
const url = `${billingApiUrl.replace(/\/+$/, "")}/v1/checkouts/sessions`;
|
|
1896
1917
|
const controller = new AbortController();
|
|
1897
1918
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1919
|
+
const headers = { "Content-Type": "application/json" };
|
|
1920
|
+
if (resolvedIdempotencyKey) {
|
|
1921
|
+
headers[IDEMPOTENCY_KEY_HEADER2] = resolvedIdempotencyKey;
|
|
1922
|
+
}
|
|
1898
1923
|
let status;
|
|
1899
1924
|
let body;
|
|
1900
1925
|
try {
|
|
1901
1926
|
const response = await fetch(url, {
|
|
1902
1927
|
method: "POST",
|
|
1903
|
-
headers
|
|
1928
|
+
headers,
|
|
1904
1929
|
body: JSON.stringify(payload),
|
|
1905
1930
|
signal: controller.signal
|
|
1906
1931
|
});
|
|
@@ -1970,13 +1995,19 @@ async function createCheckoutSessionWithRetries(options) {
|
|
|
1970
1995
|
if (!Number.isFinite(maxRetries) || !Number.isInteger(maxRetries) || maxRetries <= 0) {
|
|
1971
1996
|
throw new Error("Number of retries must be greater than 0");
|
|
1972
1997
|
}
|
|
1998
|
+
const attemptOptions = {
|
|
1999
|
+
...sessionOptions,
|
|
2000
|
+
idempotencyKey: resolveIdempotencyKey2(sessionOptions.idempotencyKey)
|
|
2001
|
+
};
|
|
1973
2002
|
let lastErr;
|
|
1974
2003
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1975
2004
|
try {
|
|
1976
|
-
return await createCheckoutSession(
|
|
2005
|
+
return await createCheckoutSession(attemptOptions);
|
|
1977
2006
|
} catch (err) {
|
|
1978
2007
|
lastErr = err;
|
|
1979
|
-
|
|
2008
|
+
const isTransportAbort = err instanceof Error && err.name === "AbortError";
|
|
2009
|
+
const isInProgressReplay = err instanceof FloPayError7 && err.code === IDEMPOTENCY_IN_PROGRESS_CODE2;
|
|
2010
|
+
if ((isTransportAbort || isInProgressReplay) && attempt < maxRetries) {
|
|
1980
2011
|
await new Promise((r) => setTimeout(r, 100 * Math.pow(2, attempt)));
|
|
1981
2012
|
continue;
|
|
1982
2013
|
}
|