@flopay/js 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 +39 -0
- package/dist/index.cjs +79 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.mjs +85 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -432,6 +432,9 @@ declare class PaymentAPI {
|
|
|
432
432
|
};
|
|
433
433
|
avsCheck?: boolean;
|
|
434
434
|
avsConfig?: Record<string, unknown>;
|
|
435
|
+
}, options?: {
|
|
436
|
+
signal?: AbortSignal;
|
|
437
|
+
timeoutMs?: number;
|
|
435
438
|
}): Promise<void>;
|
|
436
439
|
/**
|
|
437
440
|
* Create a PaymentIntent on the backend.
|
|
@@ -692,6 +695,13 @@ declare function createCheckoutSession(options: CreateSessionParams): Promise<Ch
|
|
|
692
695
|
* Creates a checkout session with automatic retry on timeout/abort errors.
|
|
693
696
|
*
|
|
694
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.
|
|
695
705
|
*/
|
|
696
706
|
declare function createCheckoutSessionWithRetries(options: CreateSessionParams & {
|
|
697
707
|
maxRetries?: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -432,6 +432,9 @@ declare class PaymentAPI {
|
|
|
432
432
|
};
|
|
433
433
|
avsCheck?: boolean;
|
|
434
434
|
avsConfig?: Record<string, unknown>;
|
|
435
|
+
}, options?: {
|
|
436
|
+
signal?: AbortSignal;
|
|
437
|
+
timeoutMs?: number;
|
|
435
438
|
}): Promise<void>;
|
|
436
439
|
/**
|
|
437
440
|
* Create a PaymentIntent on the backend.
|
|
@@ -692,6 +695,13 @@ declare function createCheckoutSession(options: CreateSessionParams): Promise<Ch
|
|
|
692
695
|
* Creates a checkout session with automatic retry on timeout/abort errors.
|
|
693
696
|
*
|
|
694
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.
|
|
695
705
|
*/
|
|
696
706
|
declare function createCheckoutSessionWithRetries(options: CreateSessionParams & {
|
|
697
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
|
|
|
@@ -593,6 +596,7 @@ var DEFAULT_PROCESSING_RETRY_AFTER_MS = 1e3;
|
|
|
593
596
|
var MIN_PROCESSING_RETRY_AFTER_MS = 500;
|
|
594
597
|
var DEFAULT_PROCESSING_TIMEOUT_MS = 15e3;
|
|
595
598
|
var MAX_PROCESSING_RETRY_AFTER_MS = 3e3;
|
|
599
|
+
var DEFAULT_ACCOUNT_SNAPSHOT_TIMEOUT_MS = 1e4;
|
|
596
600
|
function isRecord(value) {
|
|
597
601
|
return typeof value === "object" && value !== null;
|
|
598
602
|
}
|
|
@@ -634,6 +638,7 @@ async function buildApiErrorFromResponse(response, fallbackMessage) {
|
|
|
634
638
|
});
|
|
635
639
|
}
|
|
636
640
|
var NETWORK_RETRY_ATTEMPTS = 2;
|
|
641
|
+
var IDEMPOTENCY_IN_PROGRESS_RETRY_ATTEMPTS = 2;
|
|
637
642
|
async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEMPTS) {
|
|
638
643
|
let lastErr;
|
|
639
644
|
for (let attempt = 0; ; attempt++) {
|
|
@@ -815,18 +820,33 @@ var PaymentAPI = class {
|
|
|
815
820
|
* pre-pay PATCH would silently leave AVS unsent and cause an
|
|
816
821
|
* AVS-protected charge to decline downstream.
|
|
817
822
|
*/
|
|
818
|
-
async patchAccountSnapshot(sessionId, nonce, body) {
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
823
|
+
async patchAccountSnapshot(sessionId, nonce, body, options) {
|
|
824
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_ACCOUNT_SNAPSHOT_TIMEOUT_MS;
|
|
825
|
+
const controller = new AbortController();
|
|
826
|
+
const onCallerAbort = () => controller.abort();
|
|
827
|
+
if (options?.signal) {
|
|
828
|
+
if (options.signal.aborted) controller.abort();
|
|
829
|
+
else options.signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
830
|
+
}
|
|
831
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
832
|
+
let response;
|
|
833
|
+
try {
|
|
834
|
+
response = await fetchWithNetworkRetry(
|
|
835
|
+
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(sessionId)}/account`,
|
|
836
|
+
{
|
|
837
|
+
method: "PATCH",
|
|
838
|
+
headers: {
|
|
839
|
+
"Content-Type": "application/json",
|
|
840
|
+
"x-checkout-session-token": nonce
|
|
841
|
+
},
|
|
842
|
+
body: JSON.stringify(body),
|
|
843
|
+
signal: controller.signal
|
|
844
|
+
}
|
|
845
|
+
);
|
|
846
|
+
} finally {
|
|
847
|
+
clearTimeout(timer);
|
|
848
|
+
options?.signal?.removeEventListener("abort", onCallerAbort);
|
|
849
|
+
}
|
|
830
850
|
if (!response.ok) {
|
|
831
851
|
throw await buildApiErrorFromResponse(response, "Failed to persist account snapshot");
|
|
832
852
|
}
|
|
@@ -965,29 +985,41 @@ var PaymentAPI = class {
|
|
|
965
985
|
if (params.checkoutType) payload["checkoutType"] = params.checkoutType;
|
|
966
986
|
if (params.checkoutLayout) payload["checkoutLayout"] = params.checkoutLayout;
|
|
967
987
|
if (params.avsConfig) payload["avsConfig"] = params.avsConfig;
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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;
|
|
998
|
+
}
|
|
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
|
+
}
|
|
987
1008
|
);
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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;
|
|
991
1023
|
}
|
|
992
1024
|
const body = await response.json();
|
|
993
1025
|
if (body.data && "gateways" in body.data) {
|
|
@@ -1786,9 +1818,12 @@ async function loadFloPay(publishableKey, options) {
|
|
|
1786
1818
|
// src/create-checkout-session.ts
|
|
1787
1819
|
import {
|
|
1788
1820
|
FloPayError as FloPayError7,
|
|
1821
|
+
IDEMPOTENCY_IN_PROGRESS_CODE as IDEMPOTENCY_IN_PROGRESS_CODE2,
|
|
1822
|
+
IDEMPOTENCY_KEY_HEADER as IDEMPOTENCY_KEY_HEADER2,
|
|
1789
1823
|
SDK_VERSION as SDK_VERSION2,
|
|
1790
1824
|
buildProductPayload as buildProductPayload2,
|
|
1791
1825
|
foldIntoProducts as foldIntoProducts2,
|
|
1826
|
+
resolveIdempotencyKey as resolveIdempotencyKey2,
|
|
1792
1827
|
resolveSessionCurrency as resolveSessionCurrency2
|
|
1793
1828
|
} from "@flopay/shared";
|
|
1794
1829
|
var MAX_COUPON_CODES = 5;
|
|
@@ -1829,8 +1864,10 @@ async function createCheckoutSession(options) {
|
|
|
1829
1864
|
timeoutMs = 12e3,
|
|
1830
1865
|
clientId,
|
|
1831
1866
|
currency,
|
|
1832
|
-
utmMetadata
|
|
1867
|
+
utmMetadata,
|
|
1868
|
+
idempotencyKey
|
|
1833
1869
|
} = options;
|
|
1870
|
+
const resolvedIdempotencyKey = resolveIdempotencyKey2(idempotencyKey);
|
|
1834
1871
|
if (couponCodes.length > MAX_COUPON_CODES) {
|
|
1835
1872
|
throw new FloPayError7(
|
|
1836
1873
|
`Too many coupon codes \u2014 a checkout session accepts at most ${MAX_COUPON_CODES}.`,
|
|
@@ -1879,12 +1916,16 @@ async function createCheckoutSession(options) {
|
|
|
1879
1916
|
const url = `${billingApiUrl.replace(/\/+$/, "")}/v1/checkouts/sessions`;
|
|
1880
1917
|
const controller = new AbortController();
|
|
1881
1918
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1919
|
+
const headers = { "Content-Type": "application/json" };
|
|
1920
|
+
if (resolvedIdempotencyKey) {
|
|
1921
|
+
headers[IDEMPOTENCY_KEY_HEADER2] = resolvedIdempotencyKey;
|
|
1922
|
+
}
|
|
1882
1923
|
let status;
|
|
1883
1924
|
let body;
|
|
1884
1925
|
try {
|
|
1885
1926
|
const response = await fetch(url, {
|
|
1886
1927
|
method: "POST",
|
|
1887
|
-
headers
|
|
1928
|
+
headers,
|
|
1888
1929
|
body: JSON.stringify(payload),
|
|
1889
1930
|
signal: controller.signal
|
|
1890
1931
|
});
|
|
@@ -1954,13 +1995,19 @@ async function createCheckoutSessionWithRetries(options) {
|
|
|
1954
1995
|
if (!Number.isFinite(maxRetries) || !Number.isInteger(maxRetries) || maxRetries <= 0) {
|
|
1955
1996
|
throw new Error("Number of retries must be greater than 0");
|
|
1956
1997
|
}
|
|
1998
|
+
const attemptOptions = {
|
|
1999
|
+
...sessionOptions,
|
|
2000
|
+
idempotencyKey: resolveIdempotencyKey2(sessionOptions.idempotencyKey)
|
|
2001
|
+
};
|
|
1957
2002
|
let lastErr;
|
|
1958
2003
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1959
2004
|
try {
|
|
1960
|
-
return await createCheckoutSession(
|
|
2005
|
+
return await createCheckoutSession(attemptOptions);
|
|
1961
2006
|
} catch (err) {
|
|
1962
2007
|
lastErr = err;
|
|
1963
|
-
|
|
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) {
|
|
1964
2011
|
await new Promise((r) => setTimeout(r, 100 * Math.pow(2, attempt)));
|
|
1965
2012
|
continue;
|
|
1966
2013
|
}
|