@funnelfox/billing 0.5.0-beta.3 → 0.5.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/dist/chunk-index.cjs.js +4 -5
- package/dist/chunk-index.cjs2.js +10 -3
- package/dist/chunk-index.es.js +4 -5
- package/dist/chunk-index.es2.js +10 -3
- package/dist/funnelfox-billing.cjs.js +175 -17
- package/dist/funnelfox-billing.esm.js +175 -17
- package/dist/funnelfox-billing.js +189 -25
- package/dist/funnelfox-billing.min.js +1 -1
- package/package.json +4 -6
- package/src/types.d.ts +12 -15
|
@@ -202,6 +202,132 @@ function withTimeout(promise, timeoutMs, message = 'Operation timed out') {
|
|
|
202
202
|
return Promise.race([promise, timeoutPromise]);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* @fileoverview Dynamic loader for Primer SDK
|
|
207
|
+
* Loads Primer script and CSS from CDN independently of bundler
|
|
208
|
+
*/
|
|
209
|
+
const PRIMER_CDN_BASE = 'https://sdk.primer.io/web';
|
|
210
|
+
const DEFAULT_VERSION = '2.57.3';
|
|
211
|
+
// Integrity hashes for specific versions (for SRI security)
|
|
212
|
+
const INTEGRITY_HASHES = {
|
|
213
|
+
'2.57.3': {
|
|
214
|
+
js: 'sha384-xq2SWkYvTlKOMpuXQUXq1QI3eZN7JiqQ3Sc72U9wY1IE30MW3HkwQWg/1n6BTMz4',
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
let loadingPromise = null;
|
|
218
|
+
let isLoaded = false;
|
|
219
|
+
/**
|
|
220
|
+
* Injects a script tag into the document head
|
|
221
|
+
*/
|
|
222
|
+
function injectScript(src, integrity) {
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
// Check if script already exists
|
|
225
|
+
const existingScript = document.querySelector(`script[src="${src}"]`);
|
|
226
|
+
if (existingScript) {
|
|
227
|
+
resolve(existingScript);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const script = document.createElement('script');
|
|
231
|
+
script.src = src;
|
|
232
|
+
script.async = true;
|
|
233
|
+
script.crossOrigin = 'anonymous';
|
|
234
|
+
if (integrity) {
|
|
235
|
+
script.integrity = integrity;
|
|
236
|
+
}
|
|
237
|
+
script.onload = () => resolve(script);
|
|
238
|
+
script.onerror = () => reject(new Error(`Failed to load Primer SDK script from ${src}`));
|
|
239
|
+
document.head.appendChild(script);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Injects a CSS link tag into the document head
|
|
244
|
+
*/
|
|
245
|
+
function injectCSS(href, integrity) {
|
|
246
|
+
return new Promise((resolve, reject) => {
|
|
247
|
+
// Check if stylesheet already exists
|
|
248
|
+
const existingLink = document.querySelector(`link[href="${href}"]`);
|
|
249
|
+
if (existingLink) {
|
|
250
|
+
resolve(existingLink);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const link = document.createElement('link');
|
|
254
|
+
link.rel = 'stylesheet';
|
|
255
|
+
link.href = href;
|
|
256
|
+
link.crossOrigin = 'anonymous';
|
|
257
|
+
if (integrity) {
|
|
258
|
+
link.integrity = integrity;
|
|
259
|
+
}
|
|
260
|
+
link.onload = () => resolve(link);
|
|
261
|
+
link.onerror = () => reject(new Error(`Failed to load Primer SDK CSS from ${href}`));
|
|
262
|
+
document.head.appendChild(link);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Waits for window.Primer to be available
|
|
267
|
+
*/
|
|
268
|
+
function waitForPrimer(timeout = 10000) {
|
|
269
|
+
return new Promise((resolve, reject) => {
|
|
270
|
+
const startTime = Date.now();
|
|
271
|
+
const check = () => {
|
|
272
|
+
if (typeof window !== 'undefined' &&
|
|
273
|
+
window.Primer &&
|
|
274
|
+
typeof window.Primer.createHeadless === 'function') {
|
|
275
|
+
resolve();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (Date.now() - startTime > timeout) {
|
|
279
|
+
reject(new Error('Timeout waiting for Primer SDK to initialize on window'));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
setTimeout(check, 50);
|
|
283
|
+
};
|
|
284
|
+
check();
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Loads the Primer SDK script and CSS from CDN
|
|
289
|
+
* @param version - The version of Primer SDK to load (default: 2.57.3)
|
|
290
|
+
* @returns Promise that resolves when SDK is loaded and ready
|
|
291
|
+
*/
|
|
292
|
+
async function loadPrimerSDK(version) {
|
|
293
|
+
// Already loaded
|
|
294
|
+
if (isLoaded) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// Already loading - return existing promise
|
|
298
|
+
if (loadingPromise) {
|
|
299
|
+
return loadingPromise;
|
|
300
|
+
}
|
|
301
|
+
// Check if Primer is already available (user may have loaded it manually)
|
|
302
|
+
if (typeof window !== 'undefined' &&
|
|
303
|
+
window.Primer &&
|
|
304
|
+
typeof window.Primer.createHeadless === 'function') {
|
|
305
|
+
isLoaded = true;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const ver = version || DEFAULT_VERSION;
|
|
309
|
+
const jsUrl = `${PRIMER_CDN_BASE}/v${ver}/Primer.min.js`;
|
|
310
|
+
const cssUrl = `${PRIMER_CDN_BASE}/v${ver}/Checkout.css`;
|
|
311
|
+
const hashes = INTEGRITY_HASHES[ver];
|
|
312
|
+
loadingPromise = (async () => {
|
|
313
|
+
try {
|
|
314
|
+
// Load CSS and JS in parallel
|
|
315
|
+
await Promise.all([
|
|
316
|
+
injectCSS(cssUrl, hashes?.css),
|
|
317
|
+
injectScript(jsUrl, hashes?.js),
|
|
318
|
+
]);
|
|
319
|
+
// Wait for Primer to be available on window
|
|
320
|
+
await waitForPrimer();
|
|
321
|
+
isLoaded = true;
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
loadingPromise = null;
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
})();
|
|
328
|
+
return loadingPromise;
|
|
329
|
+
}
|
|
330
|
+
|
|
205
331
|
var PaymentMethod;
|
|
206
332
|
(function (PaymentMethod) {
|
|
207
333
|
PaymentMethod["GOOGLE_PAY"] = "GOOGLE_PAY";
|
|
@@ -213,7 +339,7 @@ var PaymentMethod;
|
|
|
213
339
|
/**
|
|
214
340
|
* @fileoverview Constants for Funnefox SDK
|
|
215
341
|
*/
|
|
216
|
-
const SDK_VERSION = '0.5.0
|
|
342
|
+
const SDK_VERSION = '0.5.0';
|
|
217
343
|
const DEFAULTS = {
|
|
218
344
|
BASE_URL: 'https://billing.funnelfox.com',
|
|
219
345
|
REGION: 'default',
|
|
@@ -308,12 +434,28 @@ class PrimerWrapper {
|
|
|
308
434
|
this.destroyCallbacks = [];
|
|
309
435
|
this.headless = null;
|
|
310
436
|
this.availableMethods = [];
|
|
437
|
+
this.paymentMethodsInterfaces = [];
|
|
311
438
|
}
|
|
312
439
|
isPrimerAvailable() {
|
|
313
440
|
return (typeof window !== 'undefined' &&
|
|
314
441
|
window.Primer &&
|
|
315
442
|
typeof window.Primer?.createHeadless === 'function');
|
|
316
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Loads Primer SDK if not already available
|
|
446
|
+
* @param version - Optional version to load (uses default if not specified)
|
|
447
|
+
*/
|
|
448
|
+
async ensurePrimerLoaded(version) {
|
|
449
|
+
if (this.isPrimerAvailable()) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
await loadPrimerSDK(version);
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
throw new PrimerError('Failed to load Primer SDK', error);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
317
459
|
ensurePrimerAvailable() {
|
|
318
460
|
if (!this.isPrimerAvailable()) {
|
|
319
461
|
throw new PrimerError('Primer SDK not found. Please include the Primer SDK script before initializing FunnefoxSDK.');
|
|
@@ -323,7 +465,8 @@ class PrimerWrapper {
|
|
|
323
465
|
if (this.headless) {
|
|
324
466
|
return this.headless;
|
|
325
467
|
}
|
|
326
|
-
|
|
468
|
+
// Load Primer SDK if not already available
|
|
469
|
+
await this.ensurePrimerLoaded();
|
|
327
470
|
const primerOptions = merge({
|
|
328
471
|
paymentHandling: 'MANUAL',
|
|
329
472
|
apiVersion: '2.4',
|
|
@@ -376,7 +519,8 @@ class PrimerWrapper {
|
|
|
376
519
|
}
|
|
377
520
|
async renderButton(allowedPaymentMethod, { htmlNode, onMethodRenderError, onMethodRender, }) {
|
|
378
521
|
let button;
|
|
379
|
-
|
|
522
|
+
// Ensure Primer SDK is loaded
|
|
523
|
+
await this.ensurePrimerLoaded();
|
|
380
524
|
if (!this.headless) {
|
|
381
525
|
throw new PrimerError('Headless checkout not found');
|
|
382
526
|
}
|
|
@@ -411,20 +555,24 @@ class PrimerWrapper {
|
|
|
411
555
|
!options.onInputChange) {
|
|
412
556
|
throw new PrimerError('Card elements, onSubmit, and onInputChange are required for PAYMENT_CARD method');
|
|
413
557
|
}
|
|
414
|
-
|
|
558
|
+
const cardInterface = await this.renderCardCheckoutWithElements(options.cardElements, {
|
|
415
559
|
onSubmit: options.onSubmit,
|
|
416
560
|
onInputChange: options.onInputChange,
|
|
417
561
|
onMethodRenderError: options.onMethodRenderError,
|
|
418
562
|
onMethodRender: options.onMethodRender,
|
|
419
563
|
});
|
|
564
|
+
this.paymentMethodsInterfaces.push(cardInterface);
|
|
565
|
+
return cardInterface;
|
|
420
566
|
}
|
|
421
567
|
else {
|
|
422
568
|
try {
|
|
423
|
-
|
|
569
|
+
const buttonInterface = await this.renderButton(method, {
|
|
424
570
|
htmlNode,
|
|
425
571
|
onMethodRenderError: options.onMethodRenderError,
|
|
426
572
|
onMethodRender: options.onMethodRender,
|
|
427
573
|
});
|
|
574
|
+
this.paymentMethodsInterfaces.push(buttonInterface);
|
|
575
|
+
return buttonInterface;
|
|
428
576
|
}
|
|
429
577
|
catch (error) {
|
|
430
578
|
throw new PrimerError('Failed to initialize Primer checkout', error);
|
|
@@ -509,7 +657,12 @@ class PrimerWrapper {
|
|
|
509
657
|
cardNumberInput.setDisabled(disabled);
|
|
510
658
|
expiryInput.setDisabled(disabled);
|
|
511
659
|
cvvInput.setDisabled(disabled);
|
|
512
|
-
elements.button
|
|
660
|
+
if (elements.button) {
|
|
661
|
+
elements.button.disabled = disabled;
|
|
662
|
+
}
|
|
663
|
+
if (elements.cardholderName) {
|
|
664
|
+
elements.cardholderName.disabled = disabled;
|
|
665
|
+
}
|
|
513
666
|
},
|
|
514
667
|
submit: () => onSubmitHandler(),
|
|
515
668
|
destroy: () => {
|
|
@@ -551,7 +704,7 @@ class PrimerWrapper {
|
|
|
551
704
|
const { cardElements, paymentButtonElements, container, onSubmit, onInputChange, onMethodRender, onMethodRenderError, onMethodsAvailable, } = checkoutRenderOptions;
|
|
552
705
|
await this.initializeHeadlessCheckout(clientToken, checkoutOptions);
|
|
553
706
|
onMethodsAvailable?.(this.availableMethods);
|
|
554
|
-
|
|
707
|
+
await Promise.all(this.availableMethods.map(method => {
|
|
555
708
|
if (method === PaymentMethod.PAYMENT_CARD) {
|
|
556
709
|
// For card, use the main container
|
|
557
710
|
return this.initMethod(method, container, {
|
|
@@ -575,10 +728,8 @@ class PrimerWrapper {
|
|
|
575
728
|
onMethodRenderError,
|
|
576
729
|
});
|
|
577
730
|
}
|
|
578
|
-
}))
|
|
579
|
-
|
|
580
|
-
this.isInitialized = true;
|
|
581
|
-
});
|
|
731
|
+
}));
|
|
732
|
+
this.isInitialized = true;
|
|
582
733
|
}
|
|
583
734
|
wrapTokenizeHandler(handler) {
|
|
584
735
|
return async (paymentMethodTokenData, primerHandler) => {
|
|
@@ -934,6 +1085,7 @@ class CheckoutInstance extends EventEmitter {
|
|
|
934
1085
|
};
|
|
935
1086
|
this.onLoaderChangeWithRace = (state) => {
|
|
936
1087
|
const isLoading = !!(state ? ++this.counter : --this.counter);
|
|
1088
|
+
this.primerWrapper.disableButtons(isLoading);
|
|
937
1089
|
this.emit(EVENTS.LOADER_CHANGE, isLoading);
|
|
938
1090
|
};
|
|
939
1091
|
this.id = generateId('checkout_');
|
|
@@ -1017,14 +1169,15 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1017
1169
|
].join('-');
|
|
1018
1170
|
let sessionResponse;
|
|
1019
1171
|
// Return cached response if payload hasn't changed
|
|
1020
|
-
const cachedResponse = CheckoutInstance.sessionCache.get(cacheKey);
|
|
1172
|
+
const cachedResponse = await CheckoutInstance.sessionCache.get(cacheKey);
|
|
1021
1173
|
if (cachedResponse) {
|
|
1022
1174
|
sessionResponse = cachedResponse;
|
|
1023
1175
|
}
|
|
1024
1176
|
else {
|
|
1025
|
-
|
|
1177
|
+
const sessionRequest = this.apiClient.createClientSession(sessionParams);
|
|
1026
1178
|
// Cache the successful response
|
|
1027
|
-
CheckoutInstance.sessionCache.set(cacheKey,
|
|
1179
|
+
CheckoutInstance.sessionCache.set(cacheKey, sessionRequest);
|
|
1180
|
+
sessionResponse = await sessionRequest;
|
|
1028
1181
|
}
|
|
1029
1182
|
const sessionData = this.apiClient.processSessionResponse(sessionResponse);
|
|
1030
1183
|
this.orderId = sessionData.orderId;
|
|
@@ -1253,18 +1406,18 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1253
1406
|
async getDefaultSkinCheckoutOptions() {
|
|
1254
1407
|
const skinFactory = (await import('./chunk-index.es.js'))
|
|
1255
1408
|
.default;
|
|
1256
|
-
const skin = await skinFactory(this.
|
|
1409
|
+
const skin = await skinFactory(this.checkoutConfig);
|
|
1257
1410
|
this.on(EVENTS.INPUT_ERROR, skin.onInputError);
|
|
1258
1411
|
this.on(EVENTS.STATUS_CHANGE, skin.onStatusChange);
|
|
1259
1412
|
this.on(EVENTS.ERROR, (error) => skin.onError(error));
|
|
1260
1413
|
this.on(EVENTS.LOADER_CHANGE, skin.onLoaderChange);
|
|
1261
1414
|
this.on(EVENTS.DESTROY, skin.onDestroy);
|
|
1262
|
-
this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
|
|
1263
1415
|
this.on(EVENTS.SUCCESS, skin.onSuccess);
|
|
1264
1416
|
this.on(EVENTS.START_PURCHASE, skin.onStartPurchase);
|
|
1265
1417
|
this.on(EVENTS.PURCHASE_FAILURE, skin.onPurchaseFailure);
|
|
1266
1418
|
this.on(EVENTS.PURCHASE_COMPLETED, skin.onPurchaseCompleted);
|
|
1267
1419
|
this.on(EVENTS.METHODS_AVAILABLE, skin.onMethodsAvailable);
|
|
1420
|
+
this.on(EVENTS.METHODS_AVAILABLE, this.hideInitializingLoader);
|
|
1268
1421
|
return skin.getCheckoutOptions();
|
|
1269
1422
|
}
|
|
1270
1423
|
async getCardDefaultSkinCheckoutOptions(node) {
|
|
@@ -1273,6 +1426,7 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1273
1426
|
skin.init();
|
|
1274
1427
|
this.on(EVENTS.INPUT_ERROR, skin.onInputError);
|
|
1275
1428
|
this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
|
|
1429
|
+
this.on(EVENTS.SUCCESS, skin.onDestroy);
|
|
1276
1430
|
return skin.getCheckoutOptions();
|
|
1277
1431
|
}
|
|
1278
1432
|
showInitializingLoader() {
|
|
@@ -1307,8 +1461,9 @@ function resolveConfig(options, functionName) {
|
|
|
1307
1461
|
}
|
|
1308
1462
|
async function createCheckout(options) {
|
|
1309
1463
|
const { ...checkoutConfig } = options;
|
|
1464
|
+
// Ensure Primer SDK is loaded before creating checkout
|
|
1310
1465
|
const primerWrapper = new PrimerWrapper();
|
|
1311
|
-
primerWrapper.
|
|
1466
|
+
await primerWrapper.ensurePrimerLoaded();
|
|
1312
1467
|
const config = resolveConfig(options, 'createCheckout');
|
|
1313
1468
|
const checkout = new CheckoutInstance({
|
|
1314
1469
|
...config,
|
|
@@ -1361,6 +1516,9 @@ async function silentPurchase(options) {
|
|
|
1361
1516
|
return true;
|
|
1362
1517
|
}
|
|
1363
1518
|
async function initMethod(method, element, options) {
|
|
1519
|
+
// Ensure Primer SDK is loaded before initializing payment method
|
|
1520
|
+
const primerWrapper = new PrimerWrapper();
|
|
1521
|
+
await primerWrapper.ensurePrimerLoaded();
|
|
1364
1522
|
const checkoutInstance = new CheckoutInstance({
|
|
1365
1523
|
orgId: options.orgId,
|
|
1366
1524
|
baseUrl: options.baseUrl,
|