@funnelfox/billing 0.5.0-beta.2 → 0.5.0-beta.4

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.
@@ -213,7 +213,7 @@ var PaymentMethod;
213
213
  /**
214
214
  * @fileoverview Constants for Funnefox SDK
215
215
  */
216
- const SDK_VERSION = '0.5.0-beta.2';
216
+ const SDK_VERSION = '0.5.0-beta.4';
217
217
  const DEFAULTS = {
218
218
  BASE_URL: 'https://billing.funnelfox.com',
219
219
  REGION: 'default',
@@ -245,6 +245,7 @@ const EVENTS = {
245
245
  PURCHASE_FAILURE: 'purchase-failure',
246
246
  PURCHASE_COMPLETED: 'purchase-completed',
247
247
  PURCHASE_CANCELLED: 'purchase-cancelled',
248
+ METHODS_AVAILABLE: 'methods-available',
248
249
  };
249
250
  const API_ENDPOINTS = {
250
251
  CREATE_CLIENT_SESSION: '/v1/checkout/create_client_session',
@@ -291,6 +292,12 @@ const inputStyle = {
291
292
  ({
292
293
  paddingLeft: inputStyle.input.base.paddingHorizontal + 'px',
293
294
  paddingRight: inputStyle.input.base.paddingHorizontal + 'px'});
295
+ const DEFAULT_PAYMENT_METHOD_ORDER = [
296
+ PaymentMethod.APPLE_PAY,
297
+ PaymentMethod.GOOGLE_PAY,
298
+ PaymentMethod.PAYPAL,
299
+ PaymentMethod.PAYMENT_CARD,
300
+ ];
294
301
 
295
302
  /**
296
303
  * @fileoverview Primer SDK integration wrapper
@@ -301,6 +308,7 @@ class PrimerWrapper {
301
308
  this.destroyCallbacks = [];
302
309
  this.headless = null;
303
310
  this.availableMethods = [];
311
+ this.paymentMethodsInterfaces = [];
304
312
  }
305
313
  isPrimerAvailable() {
306
314
  return (typeof window !== 'undefined' &&
@@ -333,10 +341,40 @@ class PrimerWrapper {
333
341
  disableButtons(disabled) {
334
342
  if (!this.paymentMethodsInterfaces)
335
343
  return;
336
- for (const method in this.paymentMethodsInterfaces) {
337
- this.paymentMethodsInterfaces[method].setDisabled(disabled);
344
+ for (const paymentMethodInterface of this.paymentMethodsInterfaces) {
345
+ paymentMethodInterface.setDisabled(disabled);
338
346
  }
339
347
  }
348
+ waitForPayPalReady() {
349
+ return new Promise((resolve, reject) => {
350
+ let counter = 0;
351
+ const checkPayPalEnabler = async () => {
352
+ /**
353
+ * Wait 1000 seconds for PayPal SDK to initialize
354
+ */
355
+ await new Promise(resolve => {
356
+ setTimeout(() => {
357
+ resolve();
358
+ }, 1000);
359
+ });
360
+ /**
361
+ * @link https://github.com/krakenjs/zoid/issues/334
362
+ */
363
+ // @ts-expect-error paymentMethod is private property
364
+ const isPayPalReady = !!window?.paypalPrimer?.Buttons?.instances?.[0];
365
+ if (++counter < 20 && !isPayPalReady) {
366
+ setTimeout(checkPayPalEnabler, 0);
367
+ }
368
+ else if (!isPayPalReady) {
369
+ reject(new PrimerError('PayPal paypal_js_sdk_v5_unhandled_exception was detected', PaymentMethod.PAYPAL));
370
+ }
371
+ else {
372
+ resolve();
373
+ }
374
+ };
375
+ checkPayPalEnabler();
376
+ });
377
+ }
340
378
  async renderButton(allowedPaymentMethod, { htmlNode, onMethodRenderError, onMethodRender, }) {
341
379
  let button;
342
380
  this.ensurePrimerAvailable();
@@ -350,6 +388,9 @@ class PrimerWrapper {
350
388
  }
351
389
  button = pmManager.createButton();
352
390
  await button.render(htmlNode, {});
391
+ if (allowedPaymentMethod === PaymentMethod.PAYPAL) {
392
+ await this.waitForPayPalReady();
393
+ }
353
394
  this.destroyCallbacks.push(() => button.clean());
354
395
  onMethodRender(allowedPaymentMethod);
355
396
  return {
@@ -371,20 +412,24 @@ class PrimerWrapper {
371
412
  !options.onInputChange) {
372
413
  throw new PrimerError('Card elements, onSubmit, and onInputChange are required for PAYMENT_CARD method');
373
414
  }
374
- return this.renderCardCheckoutWithElements(options.cardElements, {
415
+ const cardInterface = await this.renderCardCheckoutWithElements(options.cardElements, {
375
416
  onSubmit: options.onSubmit,
376
417
  onInputChange: options.onInputChange,
377
418
  onMethodRenderError: options.onMethodRenderError,
378
419
  onMethodRender: options.onMethodRender,
379
420
  });
421
+ this.paymentMethodsInterfaces.push(cardInterface);
422
+ return cardInterface;
380
423
  }
381
424
  else {
382
425
  try {
383
- return await this.renderButton(method, {
426
+ const buttonInterface = await this.renderButton(method, {
384
427
  htmlNode,
385
428
  onMethodRenderError: options.onMethodRenderError,
386
429
  onMethodRender: options.onMethodRender,
387
430
  });
431
+ this.paymentMethodsInterfaces.push(buttonInterface);
432
+ return buttonInterface;
388
433
  }
389
434
  catch (error) {
390
435
  throw new PrimerError('Failed to initialize Primer checkout', error);
@@ -459,8 +504,8 @@ class PrimerWrapper {
459
504
  ]);
460
505
  const onDestroy = () => {
461
506
  pmManager.removeHostedInputs();
462
- elements.cardholderName.removeEventListener('change', cardHolderOnChange);
463
- elements.button.removeEventListener('click', onSubmitHandler);
507
+ elements.cardholderName?.removeEventListener('change', cardHolderOnChange);
508
+ elements.button?.removeEventListener('click', onSubmitHandler);
464
509
  };
465
510
  this.destroyCallbacks.push(onDestroy);
466
511
  onMethodRender(PaymentMethod.PAYMENT_CARD);
@@ -469,7 +514,12 @@ class PrimerWrapper {
469
514
  cardNumberInput.setDisabled(disabled);
470
515
  expiryInput.setDisabled(disabled);
471
516
  cvvInput.setDisabled(disabled);
472
- elements.button.disabled = disabled;
517
+ if (elements.button) {
518
+ elements.button.disabled = disabled;
519
+ }
520
+ if (elements.cardholderName) {
521
+ elements.cardholderName.disabled = disabled;
522
+ }
473
523
  },
474
524
  submit: () => onSubmitHandler(),
475
525
  destroy: () => {
@@ -508,12 +558,13 @@ class PrimerWrapper {
508
558
  });
509
559
  }
510
560
  async renderCheckout(clientToken, checkoutOptions, checkoutRenderOptions) {
511
- const { cardElements, paymentButtonElements, container, onSubmit, onInputChange, onMethodRender, onMethodRenderError, } = checkoutRenderOptions;
561
+ const { cardElements, paymentButtonElements, container, onSubmit, onInputChange, onMethodRender, onMethodRenderError, onMethodsAvailable, } = checkoutRenderOptions;
512
562
  await this.initializeHeadlessCheckout(clientToken, checkoutOptions);
513
- for (const method of this.availableMethods) {
563
+ onMethodsAvailable?.(this.availableMethods);
564
+ await Promise.all(this.availableMethods.map(method => {
514
565
  if (method === PaymentMethod.PAYMENT_CARD) {
515
566
  // For card, use the main container
516
- await this.initMethod(method, container, {
567
+ return this.initMethod(method, container, {
517
568
  cardElements,
518
569
  onSubmit,
519
570
  onInputChange,
@@ -529,12 +580,12 @@ class PrimerWrapper {
529
580
  };
530
581
  // For buttons, use the specific button container element
531
582
  const buttonElement = buttonElementsMap[method];
532
- await this.initMethod(method, buttonElement, {
583
+ return this.initMethod(method, buttonElement, {
533
584
  onMethodRender,
534
585
  onMethodRenderError,
535
586
  });
536
587
  }
537
- }
588
+ }));
538
589
  this.isInitialized = true;
539
590
  }
540
591
  wrapTokenizeHandler(handler) {
@@ -830,12 +881,6 @@ class CheckoutInstance extends EventEmitter {
830
881
  constructor(config) {
831
882
  super();
832
883
  this.counter = 0;
833
- this.paymentMethodOrder = [
834
- PaymentMethod.APPLE_PAY,
835
- PaymentMethod.GOOGLE_PAY,
836
- PaymentMethod.PAYPAL,
837
- PaymentMethod.PAYMENT_CARD,
838
- ];
839
884
  this.handleInputChange = (inputName, error) => {
840
885
  this.emit(EVENTS.INPUT_ERROR, { name: inputName, error });
841
886
  };
@@ -892,8 +937,12 @@ class CheckoutInstance extends EventEmitter {
892
937
  this._setState('ready');
893
938
  }
894
939
  };
940
+ this.handleMethodsAvailable = (methods) => {
941
+ this.emit(EVENTS.METHODS_AVAILABLE, methods);
942
+ };
895
943
  this.onLoaderChangeWithRace = (state) => {
896
944
  const isLoading = !!(state ? ++this.counter : --this.counter);
945
+ this.primerWrapper.disableButtons(isLoading);
897
946
  this.emit(EVENTS.LOADER_CHANGE, isLoading);
898
947
  };
899
948
  this.id = generateId('checkout_');
@@ -961,14 +1010,32 @@ class CheckoutInstance extends EventEmitter {
961
1010
  timeout: DEFAULTS.REQUEST_TIMEOUT,
962
1011
  retryAttempts: DEFAULTS.RETRY_ATTEMPTS,
963
1012
  });
964
- const sessionResponse = await this.apiClient.createClientSession({
1013
+ const sessionParams = {
965
1014
  priceId: this.checkoutConfig.priceId,
966
1015
  externalId: this.checkoutConfig.customer.externalId,
967
1016
  email: this.checkoutConfig.customer.email,
968
1017
  region: this.region || DEFAULTS.REGION,
969
1018
  clientMetadata: this.checkoutConfig.clientMetadata,
970
1019
  countryCode: this.checkoutConfig.customer.countryCode,
971
- });
1020
+ };
1021
+ const cacheKey = [
1022
+ this.orgId,
1023
+ this.checkoutConfig.priceId,
1024
+ this.checkoutConfig.customer.externalId,
1025
+ this.checkoutConfig.customer.email,
1026
+ ].join('-');
1027
+ let sessionResponse;
1028
+ // Return cached response if payload hasn't changed
1029
+ const cachedResponse = await CheckoutInstance.sessionCache.get(cacheKey);
1030
+ if (cachedResponse) {
1031
+ sessionResponse = cachedResponse;
1032
+ }
1033
+ else {
1034
+ const sessionRequest = this.apiClient.createClientSession(sessionParams);
1035
+ // Cache the successful response
1036
+ CheckoutInstance.sessionCache.set(cacheKey, sessionRequest);
1037
+ sessionResponse = await sessionRequest;
1038
+ }
972
1039
  const sessionData = this.apiClient.processSessionResponse(sessionResponse);
973
1040
  this.orderId = sessionData.orderId;
974
1041
  this.clientToken = sessionData.clientToken;
@@ -1015,9 +1082,8 @@ class CheckoutInstance extends EventEmitter {
1015
1082
  let checkoutOptions;
1016
1083
  if (!this.checkoutConfig.cardSelectors ||
1017
1084
  !this.checkoutConfig.paymentButtonSelectors) {
1018
- if (this.checkoutConfig.paymentMethodOrder) {
1019
- this.paymentMethodOrder = this.checkoutConfig.paymentMethodOrder;
1020
- }
1085
+ this.checkoutConfig.paymentMethodOrder =
1086
+ this.checkoutConfig.paymentMethodOrder || DEFAULT_PAYMENT_METHOD_ORDER;
1021
1087
  const defaultSkinCheckoutOptions = await this.getDefaultSkinCheckoutOptions();
1022
1088
  if (!defaultSkinCheckoutOptions.cardElements ||
1023
1089
  !defaultSkinCheckoutOptions.paymentButtonElements) {
@@ -1029,14 +1095,14 @@ class CheckoutInstance extends EventEmitter {
1029
1095
  checkoutOptions = this.getCheckoutOptions(defaultSkinCheckoutOptions);
1030
1096
  }
1031
1097
  else {
1098
+ if (this.checkoutConfig.paymentMethodOrder) {
1099
+ // eslint-disable-next-line no-console
1100
+ console.warn('paymentMethodOrder is using only for default skin and will be ignored if you are using custom checkout');
1101
+ }
1032
1102
  cardElements = this.convertCardSelectorsToElements(this.checkoutConfig.cardSelectors, containerElement);
1033
1103
  paymentButtonElements = this.convertPaymentButtonSelectorsToElements(this.checkoutConfig.paymentButtonSelectors);
1034
1104
  checkoutOptions = this.getCheckoutOptions({});
1035
1105
  }
1036
- if (this.checkoutConfig.paymentMethodOrder) {
1037
- // eslint-disable-next-line no-console
1038
- console.warn('paymentMethodOrder is using only for default skin and will be ignored if you are using custom checkout');
1039
- }
1040
1106
  await this.primerWrapper.renderCheckout(this.clientToken, checkoutOptions, {
1041
1107
  container: containerElement,
1042
1108
  cardElements,
@@ -1044,6 +1110,8 @@ class CheckoutInstance extends EventEmitter {
1044
1110
  onSubmit: this.handleSubmit,
1045
1111
  onInputChange: this.handleInputChange,
1046
1112
  onMethodRender: this.handleMethodRender,
1113
+ onMethodsAvailable: this.handleMethodsAvailable,
1114
+ onMethodRenderError: this.handleMethodRenderError,
1047
1115
  });
1048
1116
  }
1049
1117
  async _processPaymentResult(result, primerHandler) {
@@ -1124,6 +1192,8 @@ class CheckoutInstance extends EventEmitter {
1124
1192
  }
1125
1193
  try {
1126
1194
  this._setState('updating');
1195
+ // Invalidate session cache
1196
+ CheckoutInstance.sessionCache.clear();
1127
1197
  await this.apiClient.updateClientSession({
1128
1198
  orderId: this.orderId,
1129
1199
  clientToken: this.clientToken,
@@ -1193,7 +1263,7 @@ class CheckoutInstance extends EventEmitter {
1193
1263
  async getDefaultSkinCheckoutOptions() {
1194
1264
  const skinFactory = (await import('./chunk-index.es.js'))
1195
1265
  .default;
1196
- const skin = await skinFactory(this.primerWrapper, this.checkoutConfig.container, this.paymentMethodOrder);
1266
+ const skin = await skinFactory(this.checkoutConfig);
1197
1267
  this.on(EVENTS.INPUT_ERROR, skin.onInputError);
1198
1268
  this.on(EVENTS.STATUS_CHANGE, skin.onStatusChange);
1199
1269
  this.on(EVENTS.ERROR, (error) => skin.onError(error));
@@ -1204,13 +1274,16 @@ class CheckoutInstance extends EventEmitter {
1204
1274
  this.on(EVENTS.START_PURCHASE, skin.onStartPurchase);
1205
1275
  this.on(EVENTS.PURCHASE_FAILURE, skin.onPurchaseFailure);
1206
1276
  this.on(EVENTS.PURCHASE_COMPLETED, skin.onPurchaseCompleted);
1277
+ this.on(EVENTS.METHODS_AVAILABLE, skin.onMethodsAvailable);
1207
1278
  return skin.getCheckoutOptions();
1208
1279
  }
1209
1280
  async getCardDefaultSkinCheckoutOptions(node) {
1210
1281
  const CardSkin = (await import('./chunk-index.es2.js')).default;
1211
- const skin = new CardSkin(node);
1282
+ const skin = new CardSkin(node, this.checkoutConfig);
1212
1283
  skin.init();
1213
1284
  this.on(EVENTS.INPUT_ERROR, skin.onInputError);
1285
+ this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
1286
+ this.on(EVENTS.SUCCESS, skin.onDestroy);
1214
1287
  return skin.getCheckoutOptions();
1215
1288
  }
1216
1289
  showInitializingLoader() {
@@ -1220,6 +1293,7 @@ class CheckoutInstance extends EventEmitter {
1220
1293
  hideLoader();
1221
1294
  }
1222
1295
  }
1296
+ CheckoutInstance.sessionCache = new Map();
1223
1297
 
1224
1298
  /**
1225
1299
  * @fileoverview Public API with configuration and orchestration logic
@@ -1275,6 +1349,28 @@ async function createClientSession(params) {
1275
1349
  });
1276
1350
  return apiClient.processSessionResponse(sessionResponse);
1277
1351
  }
1352
+ async function silentPurchase(options) {
1353
+ const { priceId, externalId, clientMetadata, orgId, baseUrl } = options;
1354
+ const apiClient = new APIClient({
1355
+ baseUrl: baseUrl,
1356
+ orgId: orgId,
1357
+ timeout: DEFAULTS.REQUEST_TIMEOUT,
1358
+ retryAttempts: DEFAULTS.RETRY_ATTEMPTS,
1359
+ });
1360
+ const response = await apiClient.oneClick({
1361
+ pp_ident: priceId,
1362
+ external_id: externalId,
1363
+ client_metadata: clientMetadata,
1364
+ });
1365
+ if (response.status !== 'success' &&
1366
+ response.error.some(({ code }) => code === 'double_purchase')) {
1367
+ throw new APIError('This product was already purchased');
1368
+ }
1369
+ else if (response.status !== 'success') {
1370
+ return false;
1371
+ }
1372
+ return true;
1373
+ }
1278
1374
  async function initMethod(method, element, options) {
1279
1375
  const checkoutInstance = new CheckoutInstance({
1280
1376
  orgId: options.orgId,
@@ -1287,6 +1383,11 @@ async function initMethod(method, element, options) {
1287
1383
  },
1288
1384
  container: '',
1289
1385
  clientMetadata: options.meta,
1386
+ card: options.card,
1387
+ style: options.style,
1388
+ applePay: options.applePay,
1389
+ paypal: options.paypal,
1390
+ googlePay: options.googlePay,
1290
1391
  },
1291
1392
  });
1292
1393
  checkoutInstance._ensureNotDestroyed();
@@ -1303,7 +1404,6 @@ async function initMethod(method, element, options) {
1303
1404
  if (method === PaymentMethod.PAYMENT_CARD) {
1304
1405
  const cardDefaultOptions = await checkoutInstance['getCardDefaultSkinCheckoutOptions'](element);
1305
1406
  const checkoutOptions = checkoutInstance['getCheckoutOptions']({
1306
- style: options.styles,
1307
1407
  ...cardDefaultOptions,
1308
1408
  });
1309
1409
  await checkoutInstance.primerWrapper.initializeHeadlessCheckout(checkoutInstance.clientToken, checkoutOptions);
@@ -1315,9 +1415,7 @@ async function initMethod(method, element, options) {
1315
1415
  onMethodRenderError: checkoutInstance['handleMethodRenderError'],
1316
1416
  });
1317
1417
  }
1318
- await checkoutInstance.primerWrapper.initializeHeadlessCheckout(checkoutInstance.clientToken, checkoutInstance['getCheckoutOptions']({
1319
- style: options.styles,
1320
- }));
1418
+ await checkoutInstance.primerWrapper.initializeHeadlessCheckout(checkoutInstance.clientToken, checkoutInstance['getCheckoutOptions']({}));
1321
1419
  return checkoutInstance.primerWrapper.initMethod(method, element, {
1322
1420
  onMethodRender: checkoutInstance['handleMethodRender'],
1323
1421
  onMethodRenderError: checkoutInstance['handleMethodRenderError'],
@@ -1332,6 +1430,7 @@ const Billing = {
1332
1430
  createCheckout: createCheckout,
1333
1431
  createClientSession: createClientSession,
1334
1432
  initMethod: initMethod,
1433
+ silentPurchase: silentPurchase,
1335
1434
  };
1336
1435
  if (typeof window !== 'undefined') {
1337
1436
  window.Billing = Billing;