@funnelfox/billing 0.8.0-ffb-704.43 → 0.8.0-ffb-586.44
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/dist/chunk-index.cjs.js +8 -679
- package/dist/chunk-index.cjs3.js +1 -1
- package/dist/chunk-index.es.js +8 -679
- package/dist/chunk-index.es3.js +1 -1
- package/dist/funnelfox-billing.js +9 -680
- package/dist/funnelfox-billing.min.js +1 -1
- package/package.json +2 -2
- package/src/types.d.ts +2 -77
package/dist/chunk-index.cjs.js
CHANGED
|
@@ -561,7 +561,7 @@ exports.PaymentMethod = void 0;
|
|
|
561
561
|
/**
|
|
562
562
|
* @fileoverview Constants for Funnefox SDK
|
|
563
563
|
*/
|
|
564
|
-
const SDK_VERSION = '0.8.0-ffb-
|
|
564
|
+
const SDK_VERSION = '0.8.0-ffb-586.44';
|
|
565
565
|
const DEFAULTS = {
|
|
566
566
|
BASE_URL: 'https://billing.funnelfox.com',
|
|
567
567
|
REGION: 'default',
|
|
@@ -615,9 +615,6 @@ const ERROR_CODES = {
|
|
|
615
615
|
CONFIGURATION_ERROR: 'CONFIGURATION_ERROR',
|
|
616
616
|
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
617
617
|
};
|
|
618
|
-
const PRIMER_DEFAULTS = {
|
|
619
|
-
PAYMENT_HANDLING: 'MANUAL',
|
|
620
|
-
API_VERSION: '2.4'};
|
|
621
618
|
const ALLOWED_BUTTON_PAYMENT_METHODS = [
|
|
622
619
|
exports.PaymentMethod.GOOGLE_PAY,
|
|
623
620
|
exports.PaymentMethod.APPLE_PAY,
|
|
@@ -681,13 +678,6 @@ const APPLE_PAY_COLLECTING_EMAIL_OPTIONS = {
|
|
|
681
678
|
requiredShippingContactFields: ['emailAddress'],
|
|
682
679
|
},
|
|
683
680
|
};
|
|
684
|
-
/**
|
|
685
|
-
* Backend decline codes that mean "the 3DS check failed" (as opposed to "3DS passed, then the
|
|
686
|
-
* issuer declined"). `null`: the codes cannot tell the two apart, so any decline after a
|
|
687
|
-
* challenge is `3ds_failed` (FFB-704 spike: in production both cases share REFER_TO_CARD_ISSUER,
|
|
688
|
-
* DO_NOT_HONOR and AUTHENTICATION_REQUIRED; human decision 10).
|
|
689
|
-
*/
|
|
690
|
-
const THREE_DS_AUTH_DECLINE_CODES = null;
|
|
691
681
|
|
|
692
682
|
/**
|
|
693
683
|
* @fileoverview Input validation utilities for Funnefox SDK
|
|
@@ -1376,37 +1366,6 @@ class APIClient {
|
|
|
1376
1366
|
throw new NetworkError('Network request failed', error);
|
|
1377
1367
|
}
|
|
1378
1368
|
}
|
|
1379
|
-
/**
|
|
1380
|
-
* Sends the request exactly once: no retry and no client timeout (FFB-704, decision 9).
|
|
1381
|
-
* Same URL, headers and error classes as `request()`. Used only by `silentPurchase`'s
|
|
1382
|
-
* `handle3DS` mode for `one_click` and `resume_payment`, which must not be POSTed twice.
|
|
1383
|
-
*/
|
|
1384
|
-
async requestOnce(endpoint, options = {}) {
|
|
1385
|
-
const url = `${this.baseUrl}/${this.orgId}${endpoint}`;
|
|
1386
|
-
const requestOptions = {
|
|
1387
|
-
method: 'GET',
|
|
1388
|
-
headers: {
|
|
1389
|
-
'Content-Type': 'application/json',
|
|
1390
|
-
'X-SDK-Version': SDK_VERSION,
|
|
1391
|
-
...(options.headers || {}),
|
|
1392
|
-
},
|
|
1393
|
-
...options,
|
|
1394
|
-
};
|
|
1395
|
-
try {
|
|
1396
|
-
return await this._makeRequest(url, requestOptions);
|
|
1397
|
-
}
|
|
1398
|
-
catch (error) {
|
|
1399
|
-
if (error instanceof APIError) {
|
|
1400
|
-
// Same instance, enriched: _makeRequest leaves errorCode empty; fill it from the body.
|
|
1401
|
-
error.errorCode ?? (error.errorCode = getResponseErrorCodes(error.response)[0] ?? null);
|
|
1402
|
-
throw error;
|
|
1403
|
-
}
|
|
1404
|
-
if (error instanceof Error && error.name === 'NetworkError') {
|
|
1405
|
-
throw error;
|
|
1406
|
-
}
|
|
1407
|
-
throw new NetworkError('Network request failed', error);
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
1369
|
async _makeRequest(url, options) {
|
|
1411
1370
|
let response;
|
|
1412
1371
|
try {
|
|
@@ -1598,23 +1557,6 @@ class APIClient {
|
|
|
1598
1557
|
body: JSON.stringify(payload),
|
|
1599
1558
|
}));
|
|
1600
1559
|
}
|
|
1601
|
-
/** `one_click`, POSTed exactly once (see `requestOnce`). */
|
|
1602
|
-
async oneClickOnce(payload) {
|
|
1603
|
-
return (await this.requestOnce(API_ENDPOINTS.ONE_CLICK, {
|
|
1604
|
-
method: 'POST',
|
|
1605
|
-
body: JSON.stringify(payload),
|
|
1606
|
-
}));
|
|
1607
|
-
}
|
|
1608
|
-
/** `resume_payment`, POSTed exactly once (see `requestOnce`). */
|
|
1609
|
-
async resumePaymentOnce(params) {
|
|
1610
|
-
return (await this.requestOnce(API_ENDPOINTS.RESUME_PAYMENT, {
|
|
1611
|
-
method: 'POST',
|
|
1612
|
-
body: JSON.stringify({
|
|
1613
|
-
order_id: params.orderId,
|
|
1614
|
-
resume_token: params.resumeToken,
|
|
1615
|
-
}),
|
|
1616
|
-
}));
|
|
1617
|
-
}
|
|
1618
1560
|
async createSimpleClientSession(payload) {
|
|
1619
1561
|
return (await this.request(API_ENDPOINTS.CREATE_SIMPLE_CLIENT_SESSION, {
|
|
1620
1562
|
method: 'POST',
|
|
@@ -1626,19 +1568,6 @@ class APIClient {
|
|
|
1626
1568
|
}));
|
|
1627
1569
|
}
|
|
1628
1570
|
}
|
|
1629
|
-
/**
|
|
1630
|
-
* Returns the `error[].code` values of a backend error envelope
|
|
1631
|
-
* (`{status: 'error', error: [{msg, code, type}]}`), e.g. from `APIError.response`.
|
|
1632
|
-
*/
|
|
1633
|
-
function getResponseErrorCodes(response) {
|
|
1634
|
-
const errors = response?.error;
|
|
1635
|
-
if (!Array.isArray(errors)) {
|
|
1636
|
-
return [];
|
|
1637
|
-
}
|
|
1638
|
-
return errors
|
|
1639
|
-
.map(item => item?.code)
|
|
1640
|
-
.filter((code) => typeof code === 'string');
|
|
1641
|
-
}
|
|
1642
1571
|
|
|
1643
1572
|
class SessionService {
|
|
1644
1573
|
constructor() {
|
|
@@ -2973,278 +2902,6 @@ class CheckoutInstance extends EventEmitter {
|
|
|
2973
2902
|
}
|
|
2974
2903
|
}
|
|
2975
2904
|
|
|
2976
|
-
/**
|
|
2977
|
-
* @fileoverview 3DS challenge for one-click purchases (FFB-704). Internal module.
|
|
2978
|
-
*
|
|
2979
|
-
* Primer can run a 3DS challenge only from a tokenization handler
|
|
2980
|
-
* (`handler.continueWithNewClientToken`), and a one-click purchase has no tokenization in the
|
|
2981
|
-
* browser: the backend charges the vaulted instrument. Mechanism M1 (proven by the FFB-704 spike
|
|
2982
|
-
* on the Primer sandbox): open a customer-bound CHECKOUT client session, start a headless
|
|
2983
|
-
* checkout, tokenize a vaulted instrument through the vault manager as a stand-in, and in
|
|
2984
|
-
* `onTokenizeSuccess` continue with the one-click `action_required_token` instead. Primer then
|
|
2985
|
-
* shows its 3DS overlay; `onResumeSuccess` yields the resume token for `resume_payment`.
|
|
2986
|
-
*/
|
|
2987
|
-
/** Primer client-token intents that carry a 3DS step. */
|
|
2988
|
-
const PRIMER_3DS_INTENTS = ['3DS_AUTHENTICATION', 'PROCESSOR_3DS'];
|
|
2989
|
-
/** Default `threeDSTimeoutMs`. */
|
|
2990
|
-
const DEFAULT_THREE_DS_TIMEOUT_MS = 600000;
|
|
2991
|
-
/** Close button of Primer's 3DS overlay (Primer Web SDK 2.57.3, verified in the spike). */
|
|
2992
|
-
const PRIMER_OVERLAY_CLOSE_SELECTOR = '[data-testid="OverlayContent.closeButton"]';
|
|
2993
|
-
/** Decodes the `intent` of a Primer client token (a JWT); `null` if it is not one. */
|
|
2994
|
-
function getPrimerTokenIntent(token) {
|
|
2995
|
-
try {
|
|
2996
|
-
const parts = token.split('.');
|
|
2997
|
-
if (parts.length !== 3) {
|
|
2998
|
-
return null;
|
|
2999
|
-
}
|
|
3000
|
-
const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
3001
|
-
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
|
|
3002
|
-
const payload = JSON.parse(atob(padded));
|
|
3003
|
-
return typeof payload?.intent === 'string' ? payload.intent : null;
|
|
3004
|
-
}
|
|
3005
|
-
catch {
|
|
3006
|
-
return null;
|
|
3007
|
-
}
|
|
3008
|
-
}
|
|
3009
|
-
/** The error Primer reports when the user closes the 3DS overlay (same check as checkout.ts). */
|
|
3010
|
-
function isThreeDSCloseError(error) {
|
|
3011
|
-
return Boolean(error?.stack?.includes('PROCESSOR_3DS') &&
|
|
3012
|
-
error.code === 'RESUME_ERROR' &&
|
|
3013
|
-
error.message?.includes('fetch resume key'));
|
|
3014
|
-
}
|
|
3015
|
-
/**
|
|
3016
|
-
* Picks the vaulted instrument used as the stand-in tokenization. Primer returns the vault
|
|
3017
|
-
* oldest-first and has no "default" flag; the 3DS check runs on the action token, not on this
|
|
3018
|
-
* instrument. Cards are preferred because their vault tokenization is silent (no CVV prompt).
|
|
3019
|
-
*/
|
|
3020
|
-
function pickVaultedMethod(methods) {
|
|
3021
|
-
if (!methods || methods.length === 0) {
|
|
3022
|
-
return null;
|
|
3023
|
-
}
|
|
3024
|
-
return (methods.find(method => method.paymentInstrumentType === 'PAYMENT_CARD') ||
|
|
3025
|
-
methods[0]);
|
|
3026
|
-
}
|
|
3027
|
-
function dismissPrimerOverlay() {
|
|
3028
|
-
if (typeof document === 'undefined') {
|
|
3029
|
-
return;
|
|
3030
|
-
}
|
|
3031
|
-
const button = document.querySelector(PRIMER_OVERLAY_CLOSE_SELECTOR);
|
|
3032
|
-
button?.click();
|
|
3033
|
-
}
|
|
3034
|
-
function toPrimerError(message, error) {
|
|
3035
|
-
return error instanceof PrimerError ? error : new PrimerError(message, error);
|
|
3036
|
-
}
|
|
3037
|
-
function callSafely(fn) {
|
|
3038
|
-
try {
|
|
3039
|
-
fn();
|
|
3040
|
-
}
|
|
3041
|
-
catch {
|
|
3042
|
-
// Primer handler calls are best effort once the result is decided.
|
|
3043
|
-
}
|
|
3044
|
-
}
|
|
3045
|
-
/**
|
|
3046
|
-
* Runs the 3DS challenge for a one-click order and, when Primer hands back a resume token,
|
|
3047
|
-
* calls `resume_payment` once. Never rejects; the caller maps the result to an outcome.
|
|
3048
|
-
*/
|
|
3049
|
-
async function runOneClickThreeDS(params) {
|
|
3050
|
-
const { apiClient, region, priceId, externalId, orderId, actionToken } = params;
|
|
3051
|
-
const intent = getPrimerTokenIntent(actionToken);
|
|
3052
|
-
if (!intent || !PRIMER_3DS_INTENTS.includes(intent)) {
|
|
3053
|
-
return {
|
|
3054
|
-
kind: 'error',
|
|
3055
|
-
error: new PrimerError('Unsupported one-click action: the action token is not a Primer 3DS client token', { intent }),
|
|
3056
|
-
};
|
|
3057
|
-
}
|
|
3058
|
-
const timeoutMs = Number.isFinite(params.timeoutMs) && params.timeoutMs > 0
|
|
3059
|
-
? params.timeoutMs
|
|
3060
|
-
: DEFAULT_THREE_DS_TIMEOUT_MS;
|
|
3061
|
-
return new Promise(resolve => {
|
|
3062
|
-
let settled = false;
|
|
3063
|
-
let tokenized = false;
|
|
3064
|
-
// True only while create_client_session is in flight.
|
|
3065
|
-
let sessionInFlight = false;
|
|
3066
|
-
let timer = null;
|
|
3067
|
-
let headless = null;
|
|
3068
|
-
const settle = (result) => {
|
|
3069
|
-
if (settled) {
|
|
3070
|
-
return;
|
|
3071
|
-
}
|
|
3072
|
-
settled = true;
|
|
3073
|
-
if (timer) {
|
|
3074
|
-
clearTimeout(timer);
|
|
3075
|
-
timer = null;
|
|
3076
|
-
}
|
|
3077
|
-
headless = null;
|
|
3078
|
-
resolve(result);
|
|
3079
|
-
};
|
|
3080
|
-
timer = setTimeout(() => {
|
|
3081
|
-
timer = null;
|
|
3082
|
-
if (tokenized) {
|
|
3083
|
-
// The challenge was shown and the customer did not finish it.
|
|
3084
|
-
settle({ kind: 'abandoned' });
|
|
3085
|
-
// Close Primer's overlay; the close error it raises is ignored (already settled).
|
|
3086
|
-
dismissPrimerOverlay();
|
|
3087
|
-
return;
|
|
3088
|
-
}
|
|
3089
|
-
// No challenge was shown yet: this is a setup failure, not an abandoned challenge.
|
|
3090
|
-
if (sessionInFlight) {
|
|
3091
|
-
settle({
|
|
3092
|
-
kind: 'error',
|
|
3093
|
-
error: new NetworkError('create_client_session did not respond within threeDSTimeoutMs'),
|
|
3094
|
-
failedRequest: 'create_client_session',
|
|
3095
|
-
});
|
|
3096
|
-
return;
|
|
3097
|
-
}
|
|
3098
|
-
settle({
|
|
3099
|
-
kind: 'error',
|
|
3100
|
-
error: new PrimerError('Timed out before the 3DS challenge was shown (threeDSTimeoutMs)'),
|
|
3101
|
-
});
|
|
3102
|
-
}, timeoutMs);
|
|
3103
|
-
const onTokenizeSuccess = (_data, handler) => {
|
|
3104
|
-
if (settled) {
|
|
3105
|
-
callSafely(() => handler.handleFailure());
|
|
3106
|
-
return;
|
|
3107
|
-
}
|
|
3108
|
-
tokenized = true;
|
|
3109
|
-
// Ignore the stand-in vaulted token: run the one-click 3DS action instead.
|
|
3110
|
-
handler.continueWithNewClientToken(actionToken);
|
|
3111
|
-
};
|
|
3112
|
-
const onResumeSuccess = async (data, handler) => {
|
|
3113
|
-
if (settled) {
|
|
3114
|
-
callSafely(() => handler.handleFailure());
|
|
3115
|
-
return;
|
|
3116
|
-
}
|
|
3117
|
-
// The challenge is over: the timeout bounds only the challenge, never the POST.
|
|
3118
|
-
if (timer) {
|
|
3119
|
-
clearTimeout(timer);
|
|
3120
|
-
timer = null;
|
|
3121
|
-
}
|
|
3122
|
-
let response;
|
|
3123
|
-
try {
|
|
3124
|
-
response = await apiClient.resumePaymentOnce({
|
|
3125
|
-
orderId,
|
|
3126
|
-
resumeToken: data.resumeToken,
|
|
3127
|
-
});
|
|
3128
|
-
}
|
|
3129
|
-
catch (error) {
|
|
3130
|
-
callSafely(() => handler.handleFailure());
|
|
3131
|
-
settle({
|
|
3132
|
-
kind: 'error',
|
|
3133
|
-
error: error,
|
|
3134
|
-
failedRequest: 'resume_payment',
|
|
3135
|
-
});
|
|
3136
|
-
return;
|
|
3137
|
-
}
|
|
3138
|
-
const resumeData = response?.data;
|
|
3139
|
-
if (response?.status === 'success' &&
|
|
3140
|
-
resumeData?.checkout_status === 'succeeded' &&
|
|
3141
|
-
!resumeData.action_required_token) {
|
|
3142
|
-
callSafely(() => handler.handleSuccess());
|
|
3143
|
-
}
|
|
3144
|
-
else {
|
|
3145
|
-
// A chained action token is never continued (no second challenge).
|
|
3146
|
-
callSafely(() => handler.handleFailure(resumeData?.failed_message_for_user || undefined));
|
|
3147
|
-
}
|
|
3148
|
-
settle({ kind: 'resumed', response });
|
|
3149
|
-
};
|
|
3150
|
-
const onResumeError = (error) => {
|
|
3151
|
-
if (isThreeDSCloseError(error)) {
|
|
3152
|
-
settle({ kind: 'abandoned' });
|
|
3153
|
-
return;
|
|
3154
|
-
}
|
|
3155
|
-
settle({
|
|
3156
|
-
kind: 'failed',
|
|
3157
|
-
error: toPrimerError('3DS authentication failed', error),
|
|
3158
|
-
});
|
|
3159
|
-
};
|
|
3160
|
-
const onCheckoutFail = (error) => {
|
|
3161
|
-
if (tokenized) {
|
|
3162
|
-
settle({
|
|
3163
|
-
kind: 'failed',
|
|
3164
|
-
error: toPrimerError('3DS authentication failed', error),
|
|
3165
|
-
});
|
|
3166
|
-
}
|
|
3167
|
-
else {
|
|
3168
|
-
settle({
|
|
3169
|
-
kind: 'error',
|
|
3170
|
-
error: toPrimerError('Primer checkout failed before the 3DS challenge', error),
|
|
3171
|
-
});
|
|
3172
|
-
}
|
|
3173
|
-
};
|
|
3174
|
-
const onTokenizeError = (error) => {
|
|
3175
|
-
settle({
|
|
3176
|
-
kind: 'error',
|
|
3177
|
-
error: toPrimerError('Failed to tokenize the saved payment method', error),
|
|
3178
|
-
});
|
|
3179
|
-
};
|
|
3180
|
-
(async () => {
|
|
3181
|
-
await new PrimerWrapper().ensurePrimerLoaded();
|
|
3182
|
-
if (settled) {
|
|
3183
|
-
return;
|
|
3184
|
-
}
|
|
3185
|
-
let clientToken;
|
|
3186
|
-
sessionInFlight = true;
|
|
3187
|
-
try {
|
|
3188
|
-
const session = await apiClient.createClientSession({
|
|
3189
|
-
priceId,
|
|
3190
|
-
externalId,
|
|
3191
|
-
region,
|
|
3192
|
-
clientMetadata: {},
|
|
3193
|
-
});
|
|
3194
|
-
clientToken = apiClient.processSessionResponse(session).clientToken;
|
|
3195
|
-
sessionInFlight = false;
|
|
3196
|
-
}
|
|
3197
|
-
catch (error) {
|
|
3198
|
-
sessionInFlight = false;
|
|
3199
|
-
settle({
|
|
3200
|
-
kind: 'error',
|
|
3201
|
-
error: error,
|
|
3202
|
-
failedRequest: 'create_client_session',
|
|
3203
|
-
});
|
|
3204
|
-
return;
|
|
3205
|
-
}
|
|
3206
|
-
if (settled) {
|
|
3207
|
-
return;
|
|
3208
|
-
}
|
|
3209
|
-
// A dedicated instance, not the HeadlessManager cache used by checkouts.
|
|
3210
|
-
const instance = await window.Primer.createHeadless(clientToken, {
|
|
3211
|
-
paymentHandling: PRIMER_DEFAULTS.PAYMENT_HANDLING,
|
|
3212
|
-
apiVersion: PRIMER_DEFAULTS.API_VERSION,
|
|
3213
|
-
onAvailablePaymentMethodsLoad: () => { },
|
|
3214
|
-
onTokenizeSuccess,
|
|
3215
|
-
onTokenizeError,
|
|
3216
|
-
onResumeSuccess,
|
|
3217
|
-
onResumeError,
|
|
3218
|
-
onCheckoutFail,
|
|
3219
|
-
});
|
|
3220
|
-
headless = await instance;
|
|
3221
|
-
await headless.start();
|
|
3222
|
-
if (settled) {
|
|
3223
|
-
return;
|
|
3224
|
-
}
|
|
3225
|
-
const vaultManager = headless.createVaultManager();
|
|
3226
|
-
const methods = await vaultManager.fetchVaultedPaymentMethods();
|
|
3227
|
-
const method = pickVaultedMethod(methods);
|
|
3228
|
-
if (!method) {
|
|
3229
|
-
settle({
|
|
3230
|
-
kind: 'error',
|
|
3231
|
-
error: new PrimerError('No saved payment method found for the one-click 3DS challenge'),
|
|
3232
|
-
});
|
|
3233
|
-
return;
|
|
3234
|
-
}
|
|
3235
|
-
if (settled) {
|
|
3236
|
-
return;
|
|
3237
|
-
}
|
|
3238
|
-
await vaultManager.startPaymentFlow(method.id);
|
|
3239
|
-
})().catch(error => {
|
|
3240
|
-
settle({
|
|
3241
|
-
kind: 'error',
|
|
3242
|
-
error: toPrimerError('Failed to start the one-click 3DS challenge', error),
|
|
3243
|
-
});
|
|
3244
|
-
});
|
|
3245
|
-
});
|
|
3246
|
-
}
|
|
3247
|
-
|
|
3248
2905
|
/**
|
|
3249
2906
|
* @fileoverview Public API with configuration and orchestration logic
|
|
3250
2907
|
*/
|
|
@@ -3311,46 +2968,20 @@ async function createClientSession(params) {
|
|
|
3311
2968
|
});
|
|
3312
2969
|
return apiClient.processSessionResponse(sessionResponse);
|
|
3313
2970
|
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
*/
|
|
3317
|
-
function buildOneClickRequest(options) {
|
|
3318
|
-
const payload = {
|
|
3319
|
-
pp_ident: options.priceId,
|
|
3320
|
-
external_id: options.externalId,
|
|
3321
|
-
client_metadata: options.clientMetadata,
|
|
3322
|
-
};
|
|
3323
|
-
if (options.initiationType !== undefined) {
|
|
3324
|
-
payload.initiation_type = options.initiationType;
|
|
3325
|
-
}
|
|
3326
|
-
if (options.citCardNetworks !== undefined) {
|
|
3327
|
-
payload.cit_card_networks = options.citCardNetworks;
|
|
3328
|
-
}
|
|
3329
|
-
if (options.citGooglePayPanOnly !== undefined) {
|
|
3330
|
-
payload.cit_google_pay_pan_only = options.citGooglePayPanOnly;
|
|
3331
|
-
}
|
|
3332
|
-
return payload;
|
|
3333
|
-
}
|
|
3334
|
-
function silentPurchase(options) {
|
|
3335
|
-
if (options?.handle3DS === true) {
|
|
3336
|
-
return silentPurchaseDetailed(options);
|
|
3337
|
-
}
|
|
3338
|
-
return silentPurchaseLegacy(options);
|
|
3339
|
-
}
|
|
3340
|
-
async function silentPurchaseLegacy(options) {
|
|
3341
|
-
const { orgId, baseUrl } = options;
|
|
2971
|
+
async function silentPurchase(options) {
|
|
2972
|
+
const { priceId, externalId, clientMetadata, orgId, baseUrl } = options;
|
|
3342
2973
|
const apiClient = new APIClient({
|
|
3343
2974
|
baseUrl: baseUrl,
|
|
3344
2975
|
orgId: orgId,
|
|
3345
2976
|
timeout: DEFAULTS.REQUEST_TIMEOUT,
|
|
3346
2977
|
retryAttempts: DEFAULTS.RETRY_ATTEMPTS,
|
|
3347
2978
|
});
|
|
3348
|
-
if (options.initiationType === 'cit') {
|
|
3349
|
-
// eslint-disable-next-line no-console
|
|
3350
|
-
console.warn('[FunnelfoxSDK] silentPurchase: initiationType "cit" can trigger a 3DS challenge, which only silentPurchase({ handle3DS: true }) handles.');
|
|
3351
|
-
}
|
|
3352
2979
|
try {
|
|
3353
|
-
const response = await apiClient.oneClick(
|
|
2980
|
+
const response = await apiClient.oneClick({
|
|
2981
|
+
pp_ident: priceId,
|
|
2982
|
+
external_id: externalId,
|
|
2983
|
+
client_metadata: clientMetadata,
|
|
2984
|
+
});
|
|
3354
2985
|
if (response.status !== 'success' &&
|
|
3355
2986
|
response.error.some(({ code }) => code === 'double_purchase')) {
|
|
3356
2987
|
throw new APIError('This product was already purchased');
|
|
@@ -3370,308 +3001,6 @@ async function silentPurchaseLegacy(options) {
|
|
|
3370
3001
|
throw error;
|
|
3371
3002
|
}
|
|
3372
3003
|
}
|
|
3373
|
-
const ONE_CLICK_IN_PROGRESS = 'one_click_in_progress';
|
|
3374
|
-
const DOUBLE_PURCHASE = 'double_purchase';
|
|
3375
|
-
const oneClickInFlight = new Set();
|
|
3376
|
-
function declineFields(data) {
|
|
3377
|
-
return {
|
|
3378
|
-
failedMessageForUser: data.failed_message_for_user ?? null,
|
|
3379
|
-
declineCode: data.decline_code ?? null,
|
|
3380
|
-
declineCodeSource: data.decline_code_source ?? null,
|
|
3381
|
-
};
|
|
3382
|
-
}
|
|
3383
|
-
function envelopeError(response, fallbackMessage) {
|
|
3384
|
-
const firstError = response?.error?.[0];
|
|
3385
|
-
return new APIError(firstError?.msg || fallbackMessage, null, {
|
|
3386
|
-
errorCode: firstError?.code,
|
|
3387
|
-
errorType: firstError?.type,
|
|
3388
|
-
response,
|
|
3389
|
-
});
|
|
3390
|
-
}
|
|
3391
|
-
/** Maps a 2xx `one_click` response (rows 1-4, plus "needs action"). */
|
|
3392
|
-
function mapOneClickResponse(response) {
|
|
3393
|
-
const data = response?.data;
|
|
3394
|
-
if (response?.status !== 'success' || !data) {
|
|
3395
|
-
if (getResponseErrorCodes(response).includes(DOUBLE_PURCHASE)) {
|
|
3396
|
-
return {
|
|
3397
|
-
kind: 'result',
|
|
3398
|
-
result: {
|
|
3399
|
-
outcome: 'already_purchased',
|
|
3400
|
-
orderId: null,
|
|
3401
|
-
threeDSChallenged: false,
|
|
3402
|
-
},
|
|
3403
|
-
};
|
|
3404
|
-
}
|
|
3405
|
-
return {
|
|
3406
|
-
kind: 'result',
|
|
3407
|
-
result: {
|
|
3408
|
-
outcome: 'error',
|
|
3409
|
-
orderId: data?.order_id ?? null,
|
|
3410
|
-
threeDSChallenged: false,
|
|
3411
|
-
error: envelopeError(response, 'One-click purchase failed'),
|
|
3412
|
-
failedRequest: 'one_click',
|
|
3413
|
-
},
|
|
3414
|
-
};
|
|
3415
|
-
}
|
|
3416
|
-
const orderId = data.order_id ?? null;
|
|
3417
|
-
if (data.action_required_token) {
|
|
3418
|
-
return { kind: 'action', orderId, actionToken: data.action_required_token };
|
|
3419
|
-
}
|
|
3420
|
-
switch (data.checkout_status) {
|
|
3421
|
-
case 'succeeded':
|
|
3422
|
-
return {
|
|
3423
|
-
kind: 'result',
|
|
3424
|
-
result: { outcome: 'succeeded', orderId, threeDSChallenged: false },
|
|
3425
|
-
};
|
|
3426
|
-
case 'failed':
|
|
3427
|
-
return {
|
|
3428
|
-
kind: 'result',
|
|
3429
|
-
result: {
|
|
3430
|
-
outcome: 'declined',
|
|
3431
|
-
orderId,
|
|
3432
|
-
threeDSChallenged: false,
|
|
3433
|
-
...declineFields(data),
|
|
3434
|
-
},
|
|
3435
|
-
};
|
|
3436
|
-
case 'processing':
|
|
3437
|
-
return {
|
|
3438
|
-
kind: 'result',
|
|
3439
|
-
result: { outcome: 'processing', orderId, threeDSChallenged: false },
|
|
3440
|
-
};
|
|
3441
|
-
case 'cancelled':
|
|
3442
|
-
return {
|
|
3443
|
-
kind: 'result',
|
|
3444
|
-
result: {
|
|
3445
|
-
outcome: 'error',
|
|
3446
|
-
orderId,
|
|
3447
|
-
threeDSChallenged: false,
|
|
3448
|
-
error: new APIError('Payment was cancelled', null, { response }),
|
|
3449
|
-
},
|
|
3450
|
-
};
|
|
3451
|
-
default:
|
|
3452
|
-
return {
|
|
3453
|
-
kind: 'result',
|
|
3454
|
-
result: {
|
|
3455
|
-
outcome: 'error',
|
|
3456
|
-
orderId,
|
|
3457
|
-
threeDSChallenged: false,
|
|
3458
|
-
error: new APIError(`Unhandled checkout status: ${data.checkout_status}`, null, { response }),
|
|
3459
|
-
},
|
|
3460
|
-
};
|
|
3461
|
-
}
|
|
3462
|
-
}
|
|
3463
|
-
/** Maps a `resume_payment` response after a challenge (rows 5, 6, 6b, 6c, 7, 8). */
|
|
3464
|
-
function mapResumeResponse(response, orderId, threeDSAuthDeclineCodes = THREE_DS_AUTH_DECLINE_CODES) {
|
|
3465
|
-
const data = response?.data;
|
|
3466
|
-
if (response?.status !== 'success' || !data) {
|
|
3467
|
-
return {
|
|
3468
|
-
threeDS: 'error',
|
|
3469
|
-
result: {
|
|
3470
|
-
outcome: 'error',
|
|
3471
|
-
orderId,
|
|
3472
|
-
threeDSChallenged: true,
|
|
3473
|
-
error: envelopeError(response, 'Resume payment failed'),
|
|
3474
|
-
failedRequest: 'resume_payment',
|
|
3475
|
-
},
|
|
3476
|
-
};
|
|
3477
|
-
}
|
|
3478
|
-
if (data.action_required_token) {
|
|
3479
|
-
// Chained action: not supported, no second challenge (human decision F3).
|
|
3480
|
-
return {
|
|
3481
|
-
threeDS: 'error',
|
|
3482
|
-
result: {
|
|
3483
|
-
outcome: 'error',
|
|
3484
|
-
orderId,
|
|
3485
|
-
threeDSChallenged: true,
|
|
3486
|
-
error: new FunnefoxSDKError('resume_payment returned another action token; chained 3DS is not supported', 'one_click_chained_action'),
|
|
3487
|
-
},
|
|
3488
|
-
};
|
|
3489
|
-
}
|
|
3490
|
-
switch (data.checkout_status) {
|
|
3491
|
-
case 'succeeded':
|
|
3492
|
-
return {
|
|
3493
|
-
threeDS: 'passed',
|
|
3494
|
-
result: { outcome: 'succeeded', orderId, threeDSChallenged: true },
|
|
3495
|
-
};
|
|
3496
|
-
case 'processing':
|
|
3497
|
-
return {
|
|
3498
|
-
threeDS: 'passed',
|
|
3499
|
-
result: { outcome: 'processing', orderId, threeDSChallenged: true },
|
|
3500
|
-
};
|
|
3501
|
-
case 'failed': {
|
|
3502
|
-
const authFailed = threeDSAuthDeclineCodes === null ||
|
|
3503
|
-
threeDSAuthDeclineCodes.includes(data.decline_code ?? '');
|
|
3504
|
-
return {
|
|
3505
|
-
threeDS: authFailed ? 'failed' : 'passed',
|
|
3506
|
-
result: {
|
|
3507
|
-
outcome: authFailed ? '3ds_failed' : 'declined',
|
|
3508
|
-
orderId,
|
|
3509
|
-
threeDSChallenged: true,
|
|
3510
|
-
...declineFields(data),
|
|
3511
|
-
},
|
|
3512
|
-
};
|
|
3513
|
-
}
|
|
3514
|
-
default:
|
|
3515
|
-
return {
|
|
3516
|
-
threeDS: 'error',
|
|
3517
|
-
result: {
|
|
3518
|
-
outcome: 'error',
|
|
3519
|
-
orderId,
|
|
3520
|
-
threeDSChallenged: true,
|
|
3521
|
-
error: new APIError(data.checkout_status === 'cancelled'
|
|
3522
|
-
? 'Payment was cancelled'
|
|
3523
|
-
: `Unhandled checkout status: ${data.checkout_status}`, null, { response }),
|
|
3524
|
-
},
|
|
3525
|
-
};
|
|
3526
|
-
}
|
|
3527
|
-
}
|
|
3528
|
-
function mapThreeDSRun(run, orderId) {
|
|
3529
|
-
switch (run.kind) {
|
|
3530
|
-
case 'resumed':
|
|
3531
|
-
return mapResumeResponse(run.response, orderId);
|
|
3532
|
-
case 'failed':
|
|
3533
|
-
return {
|
|
3534
|
-
threeDS: 'failed',
|
|
3535
|
-
result: {
|
|
3536
|
-
outcome: '3ds_failed',
|
|
3537
|
-
orderId,
|
|
3538
|
-
threeDSChallenged: true,
|
|
3539
|
-
error: run.error,
|
|
3540
|
-
},
|
|
3541
|
-
};
|
|
3542
|
-
case 'abandoned':
|
|
3543
|
-
return {
|
|
3544
|
-
threeDS: 'abandoned',
|
|
3545
|
-
result: { outcome: '3ds_abandoned', orderId, threeDSChallenged: true },
|
|
3546
|
-
};
|
|
3547
|
-
default: {
|
|
3548
|
-
const result = {
|
|
3549
|
-
outcome: 'error',
|
|
3550
|
-
orderId,
|
|
3551
|
-
threeDSChallenged: true,
|
|
3552
|
-
error: run.error,
|
|
3553
|
-
};
|
|
3554
|
-
if (run.failedRequest) {
|
|
3555
|
-
result.failedRequest = run.failedRequest;
|
|
3556
|
-
}
|
|
3557
|
-
return { threeDS: 'error', result };
|
|
3558
|
-
}
|
|
3559
|
-
}
|
|
3560
|
-
}
|
|
3561
|
-
function callHook(name, hook, info) {
|
|
3562
|
-
if (typeof hook !== 'function') {
|
|
3563
|
-
return;
|
|
3564
|
-
}
|
|
3565
|
-
try {
|
|
3566
|
-
hook(info);
|
|
3567
|
-
}
|
|
3568
|
-
catch (error) {
|
|
3569
|
-
// eslint-disable-next-line no-console
|
|
3570
|
-
console.error(`[FunnelfoxSDK] silentPurchase ${name} hook threw`, error);
|
|
3571
|
-
}
|
|
3572
|
-
}
|
|
3573
|
-
async function silentPurchaseDetailed(options) {
|
|
3574
|
-
for (const field of ['orgId', 'baseUrl', 'priceId', 'externalId']) {
|
|
3575
|
-
if (!options?.[field]) {
|
|
3576
|
-
throw new ValidationError(field, 'is required', options?.[field]);
|
|
3577
|
-
}
|
|
3578
|
-
}
|
|
3579
|
-
const { orgId, baseUrl, externalId } = options;
|
|
3580
|
-
const inFlightKey = `${orgId}\u0000${externalId}`;
|
|
3581
|
-
if (oneClickInFlight.has(inFlightKey)) {
|
|
3582
|
-
return {
|
|
3583
|
-
outcome: 'error',
|
|
3584
|
-
orderId: null,
|
|
3585
|
-
threeDSChallenged: false,
|
|
3586
|
-
error: new FunnefoxSDKError('A one-click purchase is already in progress for this customer', ONE_CLICK_IN_PROGRESS),
|
|
3587
|
-
};
|
|
3588
|
-
}
|
|
3589
|
-
oneClickInFlight.add(inFlightKey);
|
|
3590
|
-
try {
|
|
3591
|
-
const result = await runSilentPurchaseDetailed(options);
|
|
3592
|
-
if (result.outcome === 'error' && result.error) {
|
|
3593
|
-
const error = result.error;
|
|
3594
|
-
getErrorImage(orgId, {
|
|
3595
|
-
baseUrl,
|
|
3596
|
-
message: error.message,
|
|
3597
|
-
code: error.code,
|
|
3598
|
-
req_id: error.response?.req_id,
|
|
3599
|
-
});
|
|
3600
|
-
}
|
|
3601
|
-
return result;
|
|
3602
|
-
}
|
|
3603
|
-
finally {
|
|
3604
|
-
oneClickInFlight.delete(inFlightKey);
|
|
3605
|
-
}
|
|
3606
|
-
}
|
|
3607
|
-
async function runSilentPurchaseDetailed(options) {
|
|
3608
|
-
const apiClient = new APIClient({
|
|
3609
|
-
baseUrl: options.baseUrl,
|
|
3610
|
-
orgId: options.orgId,
|
|
3611
|
-
timeout: DEFAULTS.REQUEST_TIMEOUT,
|
|
3612
|
-
retryAttempts: DEFAULTS.RETRY_ATTEMPTS,
|
|
3613
|
-
});
|
|
3614
|
-
let response;
|
|
3615
|
-
try {
|
|
3616
|
-
response = await apiClient.oneClickOnce(buildOneClickRequest(options));
|
|
3617
|
-
}
|
|
3618
|
-
catch (error) {
|
|
3619
|
-
if (error instanceof APIError &&
|
|
3620
|
-
getResponseErrorCodes(error.response).includes(DOUBLE_PURCHASE)) {
|
|
3621
|
-
return {
|
|
3622
|
-
outcome: 'already_purchased',
|
|
3623
|
-
orderId: null,
|
|
3624
|
-
threeDSChallenged: false,
|
|
3625
|
-
};
|
|
3626
|
-
}
|
|
3627
|
-
return {
|
|
3628
|
-
outcome: 'error',
|
|
3629
|
-
orderId: null,
|
|
3630
|
-
threeDSChallenged: false,
|
|
3631
|
-
error: error,
|
|
3632
|
-
failedRequest: 'one_click',
|
|
3633
|
-
};
|
|
3634
|
-
}
|
|
3635
|
-
const step = mapOneClickResponse(response);
|
|
3636
|
-
if (step.kind === 'result') {
|
|
3637
|
-
return step.result;
|
|
3638
|
-
}
|
|
3639
|
-
const { orderId, actionToken } = step;
|
|
3640
|
-
let ended = false;
|
|
3641
|
-
const end = (result) => {
|
|
3642
|
-
if (ended) {
|
|
3643
|
-
return;
|
|
3644
|
-
}
|
|
3645
|
-
ended = true;
|
|
3646
|
-
callHook('onThreeDSEnd', options.onThreeDSEnd, { result });
|
|
3647
|
-
};
|
|
3648
|
-
callHook('onThreeDSStart', options.onThreeDSStart, { orderId });
|
|
3649
|
-
try {
|
|
3650
|
-
const run = await runOneClickThreeDS({
|
|
3651
|
-
apiClient,
|
|
3652
|
-
region: defaultConfig?.region || DEFAULTS.REGION,
|
|
3653
|
-
priceId: options.priceId,
|
|
3654
|
-
externalId: options.externalId,
|
|
3655
|
-
orderId,
|
|
3656
|
-
actionToken,
|
|
3657
|
-
timeoutMs: options.threeDSTimeoutMs ?? DEFAULT_THREE_DS_TIMEOUT_MS,
|
|
3658
|
-
});
|
|
3659
|
-
const mapped = mapThreeDSRun(run, orderId);
|
|
3660
|
-
end(mapped.threeDS);
|
|
3661
|
-
return mapped.result;
|
|
3662
|
-
}
|
|
3663
|
-
catch (error) {
|
|
3664
|
-
end('error');
|
|
3665
|
-
return {
|
|
3666
|
-
outcome: 'error',
|
|
3667
|
-
orderId,
|
|
3668
|
-
threeDSChallenged: true,
|
|
3669
|
-
error: error instanceof Error
|
|
3670
|
-
? error
|
|
3671
|
-
: new FunnefoxSDKError('One-click 3DS step failed', undefined, error),
|
|
3672
|
-
};
|
|
3673
|
-
}
|
|
3674
|
-
}
|
|
3675
3004
|
async function initMethod(method, element, options) {
|
|
3676
3005
|
try {
|
|
3677
3006
|
const checkoutInstance = new CheckoutInstance({
|