@funnelfox/billing 0.5.0-beta.2 → 0.5.0-beta.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.
@@ -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.3';
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
@@ -333,10 +340,40 @@ class PrimerWrapper {
333
340
  disableButtons(disabled) {
334
341
  if (!this.paymentMethodsInterfaces)
335
342
  return;
336
- for (const method in this.paymentMethodsInterfaces) {
337
- this.paymentMethodsInterfaces[method].setDisabled(disabled);
343
+ for (const paymentMethodInterface of this.paymentMethodsInterfaces) {
344
+ paymentMethodInterface.setDisabled(disabled);
338
345
  }
339
346
  }
347
+ waitForPayPalReady() {
348
+ return new Promise((resolve, reject) => {
349
+ let counter = 0;
350
+ const checkPayPalEnabler = async () => {
351
+ /**
352
+ * Wait 1000 seconds for PayPal SDK to initialize
353
+ */
354
+ await new Promise(resolve => {
355
+ setTimeout(() => {
356
+ resolve();
357
+ }, 1000);
358
+ });
359
+ /**
360
+ * @link https://github.com/krakenjs/zoid/issues/334
361
+ */
362
+ // @ts-expect-error paymentMethod is private property
363
+ const isPayPalReady = !!window?.paypalPrimer?.Buttons?.instances?.[0];
364
+ if (++counter < 20 && !isPayPalReady) {
365
+ setTimeout(checkPayPalEnabler, 0);
366
+ }
367
+ else if (!isPayPalReady) {
368
+ reject(new PrimerError('PayPal paypal_js_sdk_v5_unhandled_exception was detected', PaymentMethod.PAYPAL));
369
+ }
370
+ else {
371
+ resolve();
372
+ }
373
+ };
374
+ checkPayPalEnabler();
375
+ });
376
+ }
340
377
  async renderButton(allowedPaymentMethod, { htmlNode, onMethodRenderError, onMethodRender, }) {
341
378
  let button;
342
379
  this.ensurePrimerAvailable();
@@ -350,6 +387,9 @@ class PrimerWrapper {
350
387
  }
351
388
  button = pmManager.createButton();
352
389
  await button.render(htmlNode, {});
390
+ if (allowedPaymentMethod === PaymentMethod.PAYPAL) {
391
+ await this.waitForPayPalReady();
392
+ }
353
393
  this.destroyCallbacks.push(() => button.clean());
354
394
  onMethodRender(allowedPaymentMethod);
355
395
  return {
@@ -371,7 +411,7 @@ class PrimerWrapper {
371
411
  !options.onInputChange) {
372
412
  throw new PrimerError('Card elements, onSubmit, and onInputChange are required for PAYMENT_CARD method');
373
413
  }
374
- return this.renderCardCheckoutWithElements(options.cardElements, {
414
+ return await this.renderCardCheckoutWithElements(options.cardElements, {
375
415
  onSubmit: options.onSubmit,
376
416
  onInputChange: options.onInputChange,
377
417
  onMethodRenderError: options.onMethodRenderError,
@@ -459,8 +499,8 @@ class PrimerWrapper {
459
499
  ]);
460
500
  const onDestroy = () => {
461
501
  pmManager.removeHostedInputs();
462
- elements.cardholderName.removeEventListener('change', cardHolderOnChange);
463
- elements.button.removeEventListener('click', onSubmitHandler);
502
+ elements.cardholderName?.removeEventListener('change', cardHolderOnChange);
503
+ elements.button?.removeEventListener('click', onSubmitHandler);
464
504
  };
465
505
  this.destroyCallbacks.push(onDestroy);
466
506
  onMethodRender(PaymentMethod.PAYMENT_CARD);
@@ -508,12 +548,13 @@ class PrimerWrapper {
508
548
  });
509
549
  }
510
550
  async renderCheckout(clientToken, checkoutOptions, checkoutRenderOptions) {
511
- const { cardElements, paymentButtonElements, container, onSubmit, onInputChange, onMethodRender, onMethodRenderError, } = checkoutRenderOptions;
551
+ const { cardElements, paymentButtonElements, container, onSubmit, onInputChange, onMethodRender, onMethodRenderError, onMethodsAvailable, } = checkoutRenderOptions;
512
552
  await this.initializeHeadlessCheckout(clientToken, checkoutOptions);
513
- for (const method of this.availableMethods) {
553
+ onMethodsAvailable?.(this.availableMethods);
554
+ return Promise.all(this.availableMethods.map(method => {
514
555
  if (method === PaymentMethod.PAYMENT_CARD) {
515
556
  // For card, use the main container
516
- await this.initMethod(method, container, {
557
+ return this.initMethod(method, container, {
517
558
  cardElements,
518
559
  onSubmit,
519
560
  onInputChange,
@@ -529,13 +570,15 @@ class PrimerWrapper {
529
570
  };
530
571
  // For buttons, use the specific button container element
531
572
  const buttonElement = buttonElementsMap[method];
532
- await this.initMethod(method, buttonElement, {
573
+ return this.initMethod(method, buttonElement, {
533
574
  onMethodRender,
534
575
  onMethodRenderError,
535
576
  });
536
577
  }
537
- }
538
- this.isInitialized = true;
578
+ })).then((interfaces) => {
579
+ this.paymentMethodsInterfaces = interfaces;
580
+ this.isInitialized = true;
581
+ });
539
582
  }
540
583
  wrapTokenizeHandler(handler) {
541
584
  return async (paymentMethodTokenData, primerHandler) => {
@@ -830,12 +873,6 @@ class CheckoutInstance extends EventEmitter {
830
873
  constructor(config) {
831
874
  super();
832
875
  this.counter = 0;
833
- this.paymentMethodOrder = [
834
- PaymentMethod.APPLE_PAY,
835
- PaymentMethod.GOOGLE_PAY,
836
- PaymentMethod.PAYPAL,
837
- PaymentMethod.PAYMENT_CARD,
838
- ];
839
876
  this.handleInputChange = (inputName, error) => {
840
877
  this.emit(EVENTS.INPUT_ERROR, { name: inputName, error });
841
878
  };
@@ -892,6 +929,9 @@ class CheckoutInstance extends EventEmitter {
892
929
  this._setState('ready');
893
930
  }
894
931
  };
932
+ this.handleMethodsAvailable = (methods) => {
933
+ this.emit(EVENTS.METHODS_AVAILABLE, methods);
934
+ };
895
935
  this.onLoaderChangeWithRace = (state) => {
896
936
  const isLoading = !!(state ? ++this.counter : --this.counter);
897
937
  this.emit(EVENTS.LOADER_CHANGE, isLoading);
@@ -961,14 +1001,31 @@ class CheckoutInstance extends EventEmitter {
961
1001
  timeout: DEFAULTS.REQUEST_TIMEOUT,
962
1002
  retryAttempts: DEFAULTS.RETRY_ATTEMPTS,
963
1003
  });
964
- const sessionResponse = await this.apiClient.createClientSession({
1004
+ const sessionParams = {
965
1005
  priceId: this.checkoutConfig.priceId,
966
1006
  externalId: this.checkoutConfig.customer.externalId,
967
1007
  email: this.checkoutConfig.customer.email,
968
1008
  region: this.region || DEFAULTS.REGION,
969
1009
  clientMetadata: this.checkoutConfig.clientMetadata,
970
1010
  countryCode: this.checkoutConfig.customer.countryCode,
971
- });
1011
+ };
1012
+ const cacheKey = [
1013
+ this.orgId,
1014
+ this.checkoutConfig.priceId,
1015
+ this.checkoutConfig.customer.externalId,
1016
+ this.checkoutConfig.customer.email,
1017
+ ].join('-');
1018
+ let sessionResponse;
1019
+ // Return cached response if payload hasn't changed
1020
+ const cachedResponse = CheckoutInstance.sessionCache.get(cacheKey);
1021
+ if (cachedResponse) {
1022
+ sessionResponse = cachedResponse;
1023
+ }
1024
+ else {
1025
+ sessionResponse = await this.apiClient.createClientSession(sessionParams);
1026
+ // Cache the successful response
1027
+ CheckoutInstance.sessionCache.set(cacheKey, sessionResponse);
1028
+ }
972
1029
  const sessionData = this.apiClient.processSessionResponse(sessionResponse);
973
1030
  this.orderId = sessionData.orderId;
974
1031
  this.clientToken = sessionData.clientToken;
@@ -1015,9 +1072,8 @@ class CheckoutInstance extends EventEmitter {
1015
1072
  let checkoutOptions;
1016
1073
  if (!this.checkoutConfig.cardSelectors ||
1017
1074
  !this.checkoutConfig.paymentButtonSelectors) {
1018
- if (this.checkoutConfig.paymentMethodOrder) {
1019
- this.paymentMethodOrder = this.checkoutConfig.paymentMethodOrder;
1020
- }
1075
+ this.checkoutConfig.paymentMethodOrder =
1076
+ this.checkoutConfig.paymentMethodOrder || DEFAULT_PAYMENT_METHOD_ORDER;
1021
1077
  const defaultSkinCheckoutOptions = await this.getDefaultSkinCheckoutOptions();
1022
1078
  if (!defaultSkinCheckoutOptions.cardElements ||
1023
1079
  !defaultSkinCheckoutOptions.paymentButtonElements) {
@@ -1029,14 +1085,14 @@ class CheckoutInstance extends EventEmitter {
1029
1085
  checkoutOptions = this.getCheckoutOptions(defaultSkinCheckoutOptions);
1030
1086
  }
1031
1087
  else {
1088
+ if (this.checkoutConfig.paymentMethodOrder) {
1089
+ // eslint-disable-next-line no-console
1090
+ console.warn('paymentMethodOrder is using only for default skin and will be ignored if you are using custom checkout');
1091
+ }
1032
1092
  cardElements = this.convertCardSelectorsToElements(this.checkoutConfig.cardSelectors, containerElement);
1033
1093
  paymentButtonElements = this.convertPaymentButtonSelectorsToElements(this.checkoutConfig.paymentButtonSelectors);
1034
1094
  checkoutOptions = this.getCheckoutOptions({});
1035
1095
  }
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
1096
  await this.primerWrapper.renderCheckout(this.clientToken, checkoutOptions, {
1041
1097
  container: containerElement,
1042
1098
  cardElements,
@@ -1044,6 +1100,8 @@ class CheckoutInstance extends EventEmitter {
1044
1100
  onSubmit: this.handleSubmit,
1045
1101
  onInputChange: this.handleInputChange,
1046
1102
  onMethodRender: this.handleMethodRender,
1103
+ onMethodsAvailable: this.handleMethodsAvailable,
1104
+ onMethodRenderError: this.handleMethodRenderError,
1047
1105
  });
1048
1106
  }
1049
1107
  async _processPaymentResult(result, primerHandler) {
@@ -1124,6 +1182,8 @@ class CheckoutInstance extends EventEmitter {
1124
1182
  }
1125
1183
  try {
1126
1184
  this._setState('updating');
1185
+ // Invalidate session cache
1186
+ CheckoutInstance.sessionCache.clear();
1127
1187
  await this.apiClient.updateClientSession({
1128
1188
  orderId: this.orderId,
1129
1189
  clientToken: this.clientToken,
@@ -1193,7 +1253,7 @@ class CheckoutInstance extends EventEmitter {
1193
1253
  async getDefaultSkinCheckoutOptions() {
1194
1254
  const skinFactory = (await import('./chunk-index.es.js'))
1195
1255
  .default;
1196
- const skin = await skinFactory(this.primerWrapper, this.checkoutConfig.container, this.paymentMethodOrder);
1256
+ const skin = await skinFactory(this.primerWrapper, this.checkoutConfig);
1197
1257
  this.on(EVENTS.INPUT_ERROR, skin.onInputError);
1198
1258
  this.on(EVENTS.STATUS_CHANGE, skin.onStatusChange);
1199
1259
  this.on(EVENTS.ERROR, (error) => skin.onError(error));
@@ -1204,13 +1264,15 @@ class CheckoutInstance extends EventEmitter {
1204
1264
  this.on(EVENTS.START_PURCHASE, skin.onStartPurchase);
1205
1265
  this.on(EVENTS.PURCHASE_FAILURE, skin.onPurchaseFailure);
1206
1266
  this.on(EVENTS.PURCHASE_COMPLETED, skin.onPurchaseCompleted);
1267
+ this.on(EVENTS.METHODS_AVAILABLE, skin.onMethodsAvailable);
1207
1268
  return skin.getCheckoutOptions();
1208
1269
  }
1209
1270
  async getCardDefaultSkinCheckoutOptions(node) {
1210
1271
  const CardSkin = (await import('./chunk-index.es2.js')).default;
1211
- const skin = new CardSkin(node);
1272
+ const skin = new CardSkin(node, this.checkoutConfig);
1212
1273
  skin.init();
1213
1274
  this.on(EVENTS.INPUT_ERROR, skin.onInputError);
1275
+ this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
1214
1276
  return skin.getCheckoutOptions();
1215
1277
  }
1216
1278
  showInitializingLoader() {
@@ -1220,6 +1282,7 @@ class CheckoutInstance extends EventEmitter {
1220
1282
  hideLoader();
1221
1283
  }
1222
1284
  }
1285
+ CheckoutInstance.sessionCache = new Map();
1223
1286
 
1224
1287
  /**
1225
1288
  * @fileoverview Public API with configuration and orchestration logic
@@ -1275,6 +1338,28 @@ async function createClientSession(params) {
1275
1338
  });
1276
1339
  return apiClient.processSessionResponse(sessionResponse);
1277
1340
  }
1341
+ async function silentPurchase(options) {
1342
+ const { priceId, externalId, clientMetadata, orgId, baseUrl } = options;
1343
+ const apiClient = new APIClient({
1344
+ baseUrl: baseUrl,
1345
+ orgId: orgId,
1346
+ timeout: DEFAULTS.REQUEST_TIMEOUT,
1347
+ retryAttempts: DEFAULTS.RETRY_ATTEMPTS,
1348
+ });
1349
+ const response = await apiClient.oneClick({
1350
+ pp_ident: priceId,
1351
+ external_id: externalId,
1352
+ client_metadata: clientMetadata,
1353
+ });
1354
+ if (response.status !== 'success' &&
1355
+ response.error.some(({ code }) => code === 'double_purchase')) {
1356
+ throw new APIError('This product was already purchased');
1357
+ }
1358
+ else if (response.status !== 'success') {
1359
+ return false;
1360
+ }
1361
+ return true;
1362
+ }
1278
1363
  async function initMethod(method, element, options) {
1279
1364
  const checkoutInstance = new CheckoutInstance({
1280
1365
  orgId: options.orgId,
@@ -1287,6 +1372,11 @@ async function initMethod(method, element, options) {
1287
1372
  },
1288
1373
  container: '',
1289
1374
  clientMetadata: options.meta,
1375
+ card: options.card,
1376
+ style: options.style,
1377
+ applePay: options.applePay,
1378
+ paypal: options.paypal,
1379
+ googlePay: options.googlePay,
1290
1380
  },
1291
1381
  });
1292
1382
  checkoutInstance._ensureNotDestroyed();
@@ -1303,7 +1393,6 @@ async function initMethod(method, element, options) {
1303
1393
  if (method === PaymentMethod.PAYMENT_CARD) {
1304
1394
  const cardDefaultOptions = await checkoutInstance['getCardDefaultSkinCheckoutOptions'](element);
1305
1395
  const checkoutOptions = checkoutInstance['getCheckoutOptions']({
1306
- style: options.styles,
1307
1396
  ...cardDefaultOptions,
1308
1397
  });
1309
1398
  await checkoutInstance.primerWrapper.initializeHeadlessCheckout(checkoutInstance.clientToken, checkoutOptions);
@@ -1315,9 +1404,7 @@ async function initMethod(method, element, options) {
1315
1404
  onMethodRenderError: checkoutInstance['handleMethodRenderError'],
1316
1405
  });
1317
1406
  }
1318
- await checkoutInstance.primerWrapper.initializeHeadlessCheckout(checkoutInstance.clientToken, checkoutInstance['getCheckoutOptions']({
1319
- style: options.styles,
1320
- }));
1407
+ await checkoutInstance.primerWrapper.initializeHeadlessCheckout(checkoutInstance.clientToken, checkoutInstance['getCheckoutOptions']({}));
1321
1408
  return checkoutInstance.primerWrapper.initMethod(method, element, {
1322
1409
  onMethodRender: checkoutInstance['handleMethodRender'],
1323
1410
  onMethodRenderError: checkoutInstance['handleMethodRenderError'],
@@ -1332,6 +1419,7 @@ const Billing = {
1332
1419
  createCheckout: createCheckout,
1333
1420
  createClientSession: createClientSession,
1334
1421
  initMethod: initMethod,
1422
+ silentPurchase: silentPurchase,
1335
1423
  };
1336
1424
  if (typeof window !== 'undefined') {
1337
1425
  window.Billing = Billing;