@funnelfox/billing 0.5.0-beta.1 → 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.1';
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
@@ -330,25 +337,44 @@ class PrimerWrapper {
330
337
  throw new PrimerError('Failed to create Primer headless checkout', error);
331
338
  }
332
339
  }
333
- initializeCardElements(selectors) {
334
- const { cardNumber, expiryDate, cvv, cardholderName, button } = selectors;
335
- return {
336
- cardNumber: document.querySelector(cardNumber),
337
- expiryDate: document.querySelector(expiryDate),
338
- cvv: document.querySelector(cvv),
339
- cardholderName: document.querySelector(cardholderName),
340
- button: document.querySelector(button),
341
- };
342
- }
343
340
  disableButtons(disabled) {
344
341
  if (!this.paymentMethodsInterfaces)
345
342
  return;
346
- for (const method in this.paymentMethodsInterfaces) {
347
- this.paymentMethodsInterfaces[method].setDisabled(disabled);
343
+ for (const paymentMethodInterface of this.paymentMethodsInterfaces) {
344
+ paymentMethodInterface.setDisabled(disabled);
348
345
  }
349
346
  }
350
- async renderButton(allowedPaymentMethod, { container, }) {
351
- const containerEl = this.validateContainer(container);
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
+ }
377
+ async renderButton(allowedPaymentMethod, { htmlNode, onMethodRenderError, onMethodRender, }) {
352
378
  let button;
353
379
  this.ensurePrimerAvailable();
354
380
  if (!this.headless) {
@@ -360,10 +386,21 @@ class PrimerWrapper {
360
386
  throw new Error('Payment method manager is not available');
361
387
  }
362
388
  button = pmManager.createButton();
363
- await button.render(containerEl, {});
389
+ await button.render(htmlNode, {});
390
+ if (allowedPaymentMethod === PaymentMethod.PAYPAL) {
391
+ await this.waitForPayPalReady();
392
+ }
364
393
  this.destroyCallbacks.push(() => button.clean());
394
+ onMethodRender(allowedPaymentMethod);
395
+ return {
396
+ setDisabled: (disabled) => {
397
+ button.setDisabled(disabled);
398
+ },
399
+ destroy: () => button.clean(),
400
+ };
365
401
  }
366
402
  catch (error) {
403
+ onMethodRenderError(allowedPaymentMethod);
367
404
  throw new PrimerError('Failed to initialize Primer checkout', error);
368
405
  }
369
406
  }
@@ -374,7 +411,7 @@ class PrimerWrapper {
374
411
  !options.onInputChange) {
375
412
  throw new PrimerError('Card elements, onSubmit, and onInputChange are required for PAYMENT_CARD method');
376
413
  }
377
- return this.renderCardCheckoutWithElements(options.cardElements, {
414
+ return await this.renderCardCheckoutWithElements(options.cardElements, {
378
415
  onSubmit: options.onSubmit,
379
416
  onInputChange: options.onInputChange,
380
417
  onMethodRenderError: options.onMethodRenderError,
@@ -382,23 +419,14 @@ class PrimerWrapper {
382
419
  });
383
420
  }
384
421
  else {
385
- // For button methods, render directly into htmlNode
386
- this.ensurePrimerAvailable();
387
- if (!this.headless) {
388
- throw new PrimerError('Headless checkout not found');
389
- }
390
422
  try {
391
- const pmManager = await this.headless.createPaymentMethodManager(method);
392
- if (!pmManager) {
393
- throw new Error('Payment method manager is not available');
394
- }
395
- const button = pmManager.createButton();
396
- await button.render(htmlNode, {});
397
- this.destroyCallbacks.push(() => button.clean());
398
- options.onMethodRender(method);
423
+ return await this.renderButton(method, {
424
+ htmlNode,
425
+ onMethodRenderError: options.onMethodRenderError,
426
+ onMethodRender: options.onMethodRender,
427
+ });
399
428
  }
400
429
  catch (error) {
401
- options.onMethodRenderError(method);
402
430
  throw new PrimerError('Failed to initialize Primer checkout', error);
403
431
  }
404
432
  }
@@ -471,8 +499,8 @@ class PrimerWrapper {
471
499
  ]);
472
500
  const onDestroy = () => {
473
501
  pmManager.removeHostedInputs();
474
- elements.cardholderName.removeEventListener('change', cardHolderOnChange);
475
- elements.button.removeEventListener('click', onSubmitHandler);
502
+ elements.cardholderName?.removeEventListener('change', cardHolderOnChange);
503
+ elements.button?.removeEventListener('click', onSubmitHandler);
476
504
  };
477
505
  this.destroyCallbacks.push(onDestroy);
478
506
  onMethodRender(PaymentMethod.PAYMENT_CARD);
@@ -491,6 +519,7 @@ class PrimerWrapper {
491
519
  };
492
520
  }
493
521
  catch (error) {
522
+ onMethodRenderError(PaymentMethod.PAYMENT_CARD);
494
523
  throw new PrimerError('Failed to initialize Primer checkout', error);
495
524
  }
496
525
  }
@@ -519,12 +548,13 @@ class PrimerWrapper {
519
548
  });
520
549
  }
521
550
  async renderCheckout(clientToken, checkoutOptions, checkoutRenderOptions) {
522
- const { cardElements, paymentButtonElements, container, onSubmit, onInputChange, onMethodRender, onMethodRenderError, } = checkoutRenderOptions;
551
+ const { cardElements, paymentButtonElements, container, onSubmit, onInputChange, onMethodRender, onMethodRenderError, onMethodsAvailable, } = checkoutRenderOptions;
523
552
  await this.initializeHeadlessCheckout(clientToken, checkoutOptions);
524
- for (const method of this.availableMethods) {
553
+ onMethodsAvailable?.(this.availableMethods);
554
+ return Promise.all(this.availableMethods.map(method => {
525
555
  if (method === PaymentMethod.PAYMENT_CARD) {
526
556
  // For card, use the main container
527
- await this.initMethod(method, container, {
557
+ return this.initMethod(method, container, {
528
558
  cardElements,
529
559
  onSubmit,
530
560
  onInputChange,
@@ -540,13 +570,15 @@ class PrimerWrapper {
540
570
  };
541
571
  // For buttons, use the specific button container element
542
572
  const buttonElement = buttonElementsMap[method];
543
- await this.initMethod(method, buttonElement, {
573
+ return this.initMethod(method, buttonElement, {
544
574
  onMethodRender,
545
575
  onMethodRenderError,
546
576
  });
547
577
  }
548
- }
549
- this.isInitialized = true;
578
+ })).then((interfaces) => {
579
+ this.paymentMethodsInterfaces = interfaces;
580
+ this.isInitialized = true;
581
+ });
550
582
  }
551
583
  wrapTokenizeHandler(handler) {
552
584
  return async (paymentMethodTokenData, primerHandler) => {
@@ -841,12 +873,6 @@ class CheckoutInstance extends EventEmitter {
841
873
  constructor(config) {
842
874
  super();
843
875
  this.counter = 0;
844
- this.paymentMethodOrder = [
845
- PaymentMethod.APPLE_PAY,
846
- PaymentMethod.GOOGLE_PAY,
847
- PaymentMethod.PAYPAL,
848
- PaymentMethod.PAYMENT_CARD,
849
- ];
850
876
  this.handleInputChange = (inputName, error) => {
851
877
  this.emit(EVENTS.INPUT_ERROR, { name: inputName, error });
852
878
  };
@@ -903,6 +929,9 @@ class CheckoutInstance extends EventEmitter {
903
929
  this._setState('ready');
904
930
  }
905
931
  };
932
+ this.handleMethodsAvailable = (methods) => {
933
+ this.emit(EVENTS.METHODS_AVAILABLE, methods);
934
+ };
906
935
  this.onLoaderChangeWithRace = (state) => {
907
936
  const isLoading = !!(state ? ++this.counter : --this.counter);
908
937
  this.emit(EVENTS.LOADER_CHANGE, isLoading);
@@ -972,14 +1001,31 @@ class CheckoutInstance extends EventEmitter {
972
1001
  timeout: DEFAULTS.REQUEST_TIMEOUT,
973
1002
  retryAttempts: DEFAULTS.RETRY_ATTEMPTS,
974
1003
  });
975
- const sessionResponse = await this.apiClient.createClientSession({
1004
+ const sessionParams = {
976
1005
  priceId: this.checkoutConfig.priceId,
977
1006
  externalId: this.checkoutConfig.customer.externalId,
978
1007
  email: this.checkoutConfig.customer.email,
979
1008
  region: this.region || DEFAULTS.REGION,
980
1009
  clientMetadata: this.checkoutConfig.clientMetadata,
981
1010
  countryCode: this.checkoutConfig.customer.countryCode,
982
- });
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
+ }
983
1029
  const sessionData = this.apiClient.processSessionResponse(sessionResponse);
984
1030
  this.orderId = sessionData.orderId;
985
1031
  this.clientToken = sessionData.clientToken;
@@ -1026,9 +1072,8 @@ class CheckoutInstance extends EventEmitter {
1026
1072
  let checkoutOptions;
1027
1073
  if (!this.checkoutConfig.cardSelectors ||
1028
1074
  !this.checkoutConfig.paymentButtonSelectors) {
1029
- if (this.checkoutConfig.paymentMethodOrder) {
1030
- this.paymentMethodOrder = this.checkoutConfig.paymentMethodOrder;
1031
- }
1075
+ this.checkoutConfig.paymentMethodOrder =
1076
+ this.checkoutConfig.paymentMethodOrder || DEFAULT_PAYMENT_METHOD_ORDER;
1032
1077
  const defaultSkinCheckoutOptions = await this.getDefaultSkinCheckoutOptions();
1033
1078
  if (!defaultSkinCheckoutOptions.cardElements ||
1034
1079
  !defaultSkinCheckoutOptions.paymentButtonElements) {
@@ -1040,14 +1085,14 @@ class CheckoutInstance extends EventEmitter {
1040
1085
  checkoutOptions = this.getCheckoutOptions(defaultSkinCheckoutOptions);
1041
1086
  }
1042
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
+ }
1043
1092
  cardElements = this.convertCardSelectorsToElements(this.checkoutConfig.cardSelectors, containerElement);
1044
1093
  paymentButtonElements = this.convertPaymentButtonSelectorsToElements(this.checkoutConfig.paymentButtonSelectors);
1045
1094
  checkoutOptions = this.getCheckoutOptions({});
1046
1095
  }
1047
- if (this.checkoutConfig.paymentMethodOrder) {
1048
- // eslint-disable-next-line no-console
1049
- console.warn('paymentMethodOrder is using only for default skin and will be ignored if you are using custom checkout');
1050
- }
1051
1096
  await this.primerWrapper.renderCheckout(this.clientToken, checkoutOptions, {
1052
1097
  container: containerElement,
1053
1098
  cardElements,
@@ -1055,6 +1100,8 @@ class CheckoutInstance extends EventEmitter {
1055
1100
  onSubmit: this.handleSubmit,
1056
1101
  onInputChange: this.handleInputChange,
1057
1102
  onMethodRender: this.handleMethodRender,
1103
+ onMethodsAvailable: this.handleMethodsAvailable,
1104
+ onMethodRenderError: this.handleMethodRenderError,
1058
1105
  });
1059
1106
  }
1060
1107
  async _processPaymentResult(result, primerHandler) {
@@ -1135,6 +1182,8 @@ class CheckoutInstance extends EventEmitter {
1135
1182
  }
1136
1183
  try {
1137
1184
  this._setState('updating');
1185
+ // Invalidate session cache
1186
+ CheckoutInstance.sessionCache.clear();
1138
1187
  await this.apiClient.updateClientSession({
1139
1188
  orderId: this.orderId,
1140
1189
  clientToken: this.clientToken,
@@ -1204,7 +1253,7 @@ class CheckoutInstance extends EventEmitter {
1204
1253
  async getDefaultSkinCheckoutOptions() {
1205
1254
  const skinFactory = (await import('./chunk-index.es.js'))
1206
1255
  .default;
1207
- const skin = await skinFactory(this.primerWrapper, this.checkoutConfig.container, this.paymentMethodOrder);
1256
+ const skin = await skinFactory(this.primerWrapper, this.checkoutConfig);
1208
1257
  this.on(EVENTS.INPUT_ERROR, skin.onInputError);
1209
1258
  this.on(EVENTS.STATUS_CHANGE, skin.onStatusChange);
1210
1259
  this.on(EVENTS.ERROR, (error) => skin.onError(error));
@@ -1215,13 +1264,15 @@ class CheckoutInstance extends EventEmitter {
1215
1264
  this.on(EVENTS.START_PURCHASE, skin.onStartPurchase);
1216
1265
  this.on(EVENTS.PURCHASE_FAILURE, skin.onPurchaseFailure);
1217
1266
  this.on(EVENTS.PURCHASE_COMPLETED, skin.onPurchaseCompleted);
1267
+ this.on(EVENTS.METHODS_AVAILABLE, skin.onMethodsAvailable);
1218
1268
  return skin.getCheckoutOptions();
1219
1269
  }
1220
1270
  async getCardDefaultSkinCheckoutOptions(node) {
1221
1271
  const CardSkin = (await import('./chunk-index.es2.js')).default;
1222
- const skin = new CardSkin(node);
1272
+ const skin = new CardSkin(node, this.checkoutConfig);
1223
1273
  skin.init();
1224
1274
  this.on(EVENTS.INPUT_ERROR, skin.onInputError);
1275
+ this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
1225
1276
  return skin.getCheckoutOptions();
1226
1277
  }
1227
1278
  showInitializingLoader() {
@@ -1231,6 +1282,7 @@ class CheckoutInstance extends EventEmitter {
1231
1282
  hideLoader();
1232
1283
  }
1233
1284
  }
1285
+ CheckoutInstance.sessionCache = new Map();
1234
1286
 
1235
1287
  /**
1236
1288
  * @fileoverview Public API with configuration and orchestration logic
@@ -1286,6 +1338,28 @@ async function createClientSession(params) {
1286
1338
  });
1287
1339
  return apiClient.processSessionResponse(sessionResponse);
1288
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
+ }
1289
1363
  async function initMethod(method, element, options) {
1290
1364
  const checkoutInstance = new CheckoutInstance({
1291
1365
  orgId: options.orgId,
@@ -1298,6 +1372,11 @@ async function initMethod(method, element, options) {
1298
1372
  },
1299
1373
  container: '',
1300
1374
  clientMetadata: options.meta,
1375
+ card: options.card,
1376
+ style: options.style,
1377
+ applePay: options.applePay,
1378
+ paypal: options.paypal,
1379
+ googlePay: options.googlePay,
1301
1380
  },
1302
1381
  });
1303
1382
  checkoutInstance._ensureNotDestroyed();
@@ -1314,7 +1393,6 @@ async function initMethod(method, element, options) {
1314
1393
  if (method === PaymentMethod.PAYMENT_CARD) {
1315
1394
  const cardDefaultOptions = await checkoutInstance['getCardDefaultSkinCheckoutOptions'](element);
1316
1395
  const checkoutOptions = checkoutInstance['getCheckoutOptions']({
1317
- style: options.styles,
1318
1396
  ...cardDefaultOptions,
1319
1397
  });
1320
1398
  await checkoutInstance.primerWrapper.initializeHeadlessCheckout(checkoutInstance.clientToken, checkoutOptions);
@@ -1326,9 +1404,7 @@ async function initMethod(method, element, options) {
1326
1404
  onMethodRenderError: checkoutInstance['handleMethodRenderError'],
1327
1405
  });
1328
1406
  }
1329
- await checkoutInstance.primerWrapper.initializeHeadlessCheckout(checkoutInstance.clientToken, checkoutInstance['getCheckoutOptions']({
1330
- style: options.styles,
1331
- }));
1407
+ await checkoutInstance.primerWrapper.initializeHeadlessCheckout(checkoutInstance.clientToken, checkoutInstance['getCheckoutOptions']({}));
1332
1408
  return checkoutInstance.primerWrapper.initMethod(method, element, {
1333
1409
  onMethodRender: checkoutInstance['handleMethodRender'],
1334
1410
  onMethodRenderError: checkoutInstance['handleMethodRenderError'],
@@ -1343,6 +1419,7 @@ const Billing = {
1343
1419
  createCheckout: createCheckout,
1344
1420
  createClientSession: createClientSession,
1345
1421
  initMethod: initMethod,
1422
+ silentPurchase: silentPurchase,
1346
1423
  };
1347
1424
  if (typeof window !== 'undefined') {
1348
1425
  window.Billing = Billing;