swell-js 5.9.4 → 5.10.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.
@@ -6295,6 +6295,10 @@
6295
6295
  }
6296
6296
  }
6297
6297
 
6298
+ /**
6299
+ * @deprecated Payment components are mounted by `@swell/apps-sdk/components`
6300
+ * at the host place `checkout.payment.methods`.
6301
+ */
6298
6302
  class AppPaymentComponent extends AppComponent {
6299
6303
  constructor(app, component, api, options) {
6300
6304
  super(app, component, api, options);
@@ -6344,6 +6348,7 @@
6344
6348
  }
6345
6349
  }
6346
6350
 
6351
+ /** @deprecated Use `@swell/apps-sdk/components` instead. */
6347
6352
  class App {
6348
6353
  constructor(app, api, options) {
6349
6354
  this.app = app;
@@ -6452,6 +6457,13 @@
6452
6457
  }
6453
6458
  }
6454
6459
 
6460
+ let deprecationWarned = false;
6461
+
6462
+ /**
6463
+ * @deprecated App components are rendered by `@swell/apps-sdk/components`
6464
+ * (`createComponents().mount()`); this module will be removed in the next
6465
+ * major version.
6466
+ */
6455
6467
  class AppController {
6456
6468
  #apps = new Map();
6457
6469
 
@@ -6460,7 +6472,15 @@
6460
6472
  this.options = options;
6461
6473
  }
6462
6474
 
6475
+ /** @deprecated Use `@swell/apps-sdk/components` instead. */
6463
6476
  async load(appId) {
6477
+ if (!deprecationWarned) {
6478
+ deprecationWarned = true;
6479
+ console.warn(
6480
+ 'swell.app is deprecated: render app components with @swell/apps-sdk/components',
6481
+ );
6482
+ }
6483
+
6464
6484
  const loadedApp = this.#apps.get(appId);
6465
6485
 
6466
6486
  if (loadedApp) {
@@ -7585,25 +7605,184 @@
7585
7605
  billing_details: billingDetails,
7586
7606
  });
7587
7607
 
7588
- return error
7589
- ? { error }
7590
- : {
7591
- gateway: 'stripe',
7592
- token: paymentMethod.id,
7593
- last4: paymentMethod.card.last4,
7594
- exp_month: paymentMethod.card.exp_month,
7595
- exp_year: paymentMethod.card.exp_year,
7596
- brand: paymentMethod.card.brand,
7597
- display_brand: paymentMethod.card.display_brand,
7598
- address_check: paymentMethod.card.checks.address_line1_check,
7599
- cvc_check: paymentMethod.card.checks.cvc_check,
7600
- zip_check: paymentMethod.card.checks.address_postal_code_check,
7601
- };
7608
+ return error ? { error } : mapCardPaymentMethod(paymentMethod);
7609
+ }
7610
+
7611
+ /**
7612
+ * Elements group options of the card Payment Element for the cart amount,
7613
+ * matching the intent StripeCardPayment#_createIntent creates.
7614
+ *
7615
+ * @param {Cart} cart
7616
+ * @returns {import('@stripe/stripe-js').StripeElementsOptionsMode}
7617
+ */
7618
+ function getCardElementsOptions(cart) {
7619
+ const { capture_total, auth_total } = cart;
7620
+ const currency = cart.currency || 'USD';
7621
+ const stripeCurrency = currency.toLowerCase();
7622
+
7623
+ // the card is only stored when the amount is not chargeable, no intent is created
7624
+ if (!isStripeChargeableAmount(capture_total, cart.currency)) {
7625
+ return {
7626
+ mode: 'setup',
7627
+ currency: stripeCurrency,
7628
+ paymentMethodTypes: ['card'],
7629
+ };
7630
+ }
7631
+
7632
+ return {
7633
+ mode: 'payment',
7634
+ amount: stripeAmountByCurrency(currency, capture_total + (auth_total || 0)),
7635
+ currency: stripeCurrency,
7636
+ captureMethod: 'manual',
7637
+ setupFutureUsage: 'off_session',
7638
+ paymentMethodTypes: ['card'],
7639
+ };
7640
+ }
7641
+
7642
+ /**
7643
+ * Elements group options of the iDEAL Payment Element,
7644
+ * matching the intent StripeIDealPayment#_createIntent creates.
7645
+ *
7646
+ * @param {Cart} cart
7647
+ * @returns {import('@stripe/stripe-js').StripeElementsOptionsMode}
7648
+ */
7649
+ function getIDealElementsOptions(cart) {
7650
+ const { capture_total } = cart;
7651
+ const currency = cart.currency || 'EUR';
7652
+
7653
+ return {
7654
+ mode: 'payment',
7655
+ amount: stripeAmountByCurrency(currency, capture_total),
7656
+ currency: currency.toLowerCase(),
7657
+ paymentMethodTypes: ['ideal'],
7658
+ };
7659
+ }
7660
+
7661
+ /**
7662
+ * Creates an Elements group without an intent and mounts the Payment Element
7663
+ * into `#${params.elementId || 'payment-element'}`.
7664
+ *
7665
+ * @param {Stripe} stripe
7666
+ * @param {import('@stripe/stripe-js').StripeElementsOptionsMode} elementsOptions
7667
+ * @param {object} params
7668
+ * @returns {import('@stripe/stripe-js').StripeElements}
7669
+ */
7670
+ function createPaymentElements(stripe, elementsOptions, params) {
7671
+ const elements = stripe.elements({
7672
+ ...params.config,
7673
+ ...elementsOptions,
7674
+ // lets stripe.createPaymentMethod read the Payment Element
7675
+ paymentMethodCreation: 'manual',
7676
+ ...(params.appearance && { appearance: params.appearance }),
7677
+ });
7678
+
7679
+ createElement('payment', elements, {
7680
+ ...params,
7681
+ options: {
7682
+ layout: 'tabs',
7683
+ fields: { billingDetails: 'never' },
7684
+ // wallets are separate express buttons
7685
+ wallets: { applePay: 'never', googlePay: 'never' },
7686
+ ...params.options,
7687
+ },
7688
+ });
7689
+
7690
+ return elements;
7691
+ }
7692
+
7693
+ /**
7694
+ * Creates a PaymentMethod from the Payment Element.
7695
+ * Brings the Elements amount up to date with the cart first.
7696
+ *
7697
+ * @param {Stripe} stripe
7698
+ * @param {import('@stripe/stripe-js').StripeElements} elements
7699
+ * @param {import('@stripe/stripe-js').StripeElementsOptionsMode} elementsOptions
7700
+ * @param {Cart} cart
7701
+ * @returns {Promise<import('@stripe/stripe-js').PaymentMethodResult>}
7702
+ */
7703
+ async function createPaymentMethodFromElements(
7704
+ stripe,
7705
+ elements,
7706
+ elementsOptions,
7707
+ cart,
7708
+ ) {
7709
+ // setup mode stores the card without an intent, there is no amount to update
7710
+ if (elementsOptions.mode === 'payment') {
7711
+ await elements.update(elementsOptions);
7712
+ }
7713
+
7714
+ const { error } = await elements.submit();
7715
+
7716
+ if (error) {
7717
+ return { error };
7718
+ }
7719
+
7720
+ const { address, ...details } = getBillingDetails(cart);
7721
+
7722
+ // fields the Payment Element does not collect must all be passed, null when unknown
7723
+ return stripe.createPaymentMethod({
7724
+ elements,
7725
+ params: {
7726
+ billing_details: {
7727
+ name: null,
7728
+ email: null,
7729
+ phone: null,
7730
+ ...details,
7731
+ address: {
7732
+ line1: null,
7733
+ line2: null,
7734
+ city: null,
7735
+ state: null,
7736
+ postal_code: null,
7737
+ country: null,
7738
+ ...address,
7739
+ },
7740
+ },
7741
+ },
7742
+ });
7743
+ }
7744
+
7745
+ /**
7746
+ * @param {Stripe} stripe
7747
+ * @param {import('@stripe/stripe-js').StripeElements} elements
7748
+ * @param {Cart} cart
7749
+ */
7750
+ async function createCardPaymentMethodFromElements(
7751
+ stripe,
7752
+ elements,
7753
+ cart,
7754
+ ) {
7755
+ const { paymentMethod, error } = await createPaymentMethodFromElements(
7756
+ stripe,
7757
+ elements,
7758
+ getCardElementsOptions(cart),
7759
+ cart,
7760
+ );
7761
+
7762
+ return error ? { error } : mapCardPaymentMethod(paymentMethod);
7763
+ }
7764
+
7765
+ /**
7766
+ * @param {import('@stripe/stripe-js').PaymentMethod} paymentMethod
7767
+ */
7768
+ function mapCardPaymentMethod(paymentMethod) {
7769
+ return {
7770
+ gateway: 'stripe',
7771
+ token: paymentMethod.id,
7772
+ last4: paymentMethod.card.last4,
7773
+ exp_month: paymentMethod.card.exp_month,
7774
+ exp_year: paymentMethod.card.exp_year,
7775
+ brand: paymentMethod.card.brand,
7776
+ display_brand: paymentMethod.card.display_brand,
7777
+ address_check: paymentMethod.card.checks.address_line1_check,
7778
+ cvc_check: paymentMethod.card.checks.cvc_check,
7779
+ zip_check: paymentMethod.card.checks.address_postal_code_check,
7780
+ };
7602
7781
  }
7603
7782
 
7604
7783
  /**
7605
7784
  * @param {Stripe} stripe
7606
- * @param {import('@stripe/stripe-js').StripeIdealBankElement} element
7785
+ * @param {import('@stripe/stripe-js').StripeIdealBankElement | {}} element `{}` when there is no idealBank element
7607
7786
  * @param {Cart} cart
7608
7787
  */
7609
7788
  async function createIDealPaymentMethod(stripe, element, cart) {
@@ -7791,6 +7970,29 @@
7791
7970
  return !minAmount || amount >= minAmount;
7792
7971
  }
7793
7972
 
7973
+ // The cart keeps what checkout v1 keeps, so the next tokenize can retrieve and reuse
7974
+ // the intent; the vault cancels the replaced ones when the order is paid
7975
+ const cartIntent = (cart, intent) => {
7976
+ const exIntent = cart.billing?.intent?.stripe;
7977
+
7978
+ return {
7979
+ id: intent.id,
7980
+ client_secret: intent.client_secret,
7981
+ amount: intent.amount,
7982
+ currency: intent.currency,
7983
+ ...(Boolean(cart.auth_total) && { auth_amount: cart.auth_total }),
7984
+ ...(exIntent?.id &&
7985
+ exIntent.id !== intent.id && {
7986
+ canceled: [...(exIntent.canceled || []), exIntent.id],
7987
+ }),
7988
+ };
7989
+ };
7990
+
7991
+ const intentAmount = ({ currency, capture_total, auth_total }) =>
7992
+ stripeAmountByCurrency(currency, capture_total + auth_total);
7993
+
7994
+ const intentCurrency = ({ currency }) => (currency || 'USD').toLowerCase();
7995
+
7794
7996
  /** @typedef {import('@stripe/stripe-js').Stripe} Stripe */
7795
7997
  /** @typedef {import('@stripe/stripe-js').StripeCardElement} StripeCardElement */
7796
7998
  /** @typedef {import('@stripe/stripe-js').StripeCardNumberElement} StripeCardNumberElement */
@@ -7834,9 +8036,30 @@
7834
8036
  StripeCardPayment.stripeElement = stripeElement;
7835
8037
  }
7836
8038
 
7837
- async createElements() {
8039
+ /** @returns {import('@stripe/stripe-js').StripeElements} */
8040
+ get stripeElements() {
8041
+ return StripeCardPayment.stripeElements;
8042
+ }
8043
+
8044
+ /** @param {import('@stripe/stripe-js').StripeElements} stripeElements */
8045
+ set stripeElements(stripeElements) {
8046
+ StripeCardPayment.stripeElements = stripeElements;
8047
+ }
8048
+
8049
+ /** @param {import('../../../types').Cart} cart */
8050
+ async createElements(cart) {
7838
8051
  await this.loadScripts(this.scripts);
7839
8052
 
8053
+ if (this.params.paymentElement) {
8054
+ this.stripeElements = createPaymentElements(
8055
+ this.stripe,
8056
+ getCardElementsOptions(cart),
8057
+ this.params,
8058
+ );
8059
+
8060
+ return;
8061
+ }
8062
+
7840
8063
  const elements = this.stripe.elements(this.params.config);
7841
8064
 
7842
8065
  if (this.params.separateElements) {
@@ -7849,18 +8072,26 @@
7849
8072
  }
7850
8073
 
7851
8074
  async tokenize() {
7852
- if (!this.stripeElement) {
8075
+ const { paymentElement, paymentMethod: token } = this.params;
8076
+
8077
+ if (token) {
8078
+ return this._authorizePaymentMethod(token);
8079
+ }
8080
+
8081
+ if (!(paymentElement ? this.stripeElements : this.stripeElement)) {
7853
8082
  throw new Error('Stripe payment element is not defined');
7854
8083
  }
7855
8084
 
7856
8085
  await this.loadScripts(this.scripts);
7857
8086
 
7858
8087
  const cart = await this.getCart();
7859
- const paymentMethod = await createPaymentMethod(
7860
- this.stripe,
7861
- this.stripeElement,
7862
- cart,
7863
- );
8088
+ const paymentMethod = paymentElement
8089
+ ? await createCardPaymentMethodFromElements(
8090
+ this.stripe,
8091
+ this.stripeElements,
8092
+ cart,
8093
+ )
8094
+ : await createPaymentMethod(this.stripe, this.stripeElement, cart);
7864
8095
 
7865
8096
  if (paymentMethod.error) {
7866
8097
  throw new Error(paymentMethod.error.message);
@@ -7870,8 +8101,12 @@
7870
8101
  paymentMethod.test = true;
7871
8102
  }
7872
8103
 
7873
- // should save payment method data when payment amount is not chargeable
7874
- if (!isStripeChargeableAmount(cart.capture_total, cart.currency)) {
8104
+ // should save payment method data when payment amount is not chargeable,
8105
+ // or when the host collects the card without authorizing it now
8106
+ if (
8107
+ !isStripeChargeableAmount(cart.capture_total, cart.currency) ||
8108
+ this.params.createIntent === false
8109
+ ) {
7875
8110
  await this.updateCart({
7876
8111
  billing: {
7877
8112
  method: 'card',
@@ -7882,60 +8117,129 @@
7882
8117
  return this.onSuccess();
7883
8118
  }
7884
8119
 
7885
- const intent = await this._createIntent(cart, paymentMethod);
8120
+ const intent = await this._authorizeIntent(cart, paymentMethod.token);
7886
8121
 
7887
8122
  await this.updateCart({
7888
8123
  billing: {
7889
8124
  method: 'card',
7890
8125
  card: paymentMethod,
7891
- intent: {
7892
- stripe: {
7893
- id: intent.id,
7894
- ...(Boolean(cart.auth_total) && {
7895
- auth_amount: cart.auth_total,
7896
- }),
7897
- },
7898
- },
8126
+ intent: { stripe: cartIntent(cart, intent) },
7899
8127
  },
7900
8128
  });
7901
8129
 
7902
8130
  this.onSuccess();
7903
8131
  }
7904
8132
 
7905
- async authenticate(payment) {
7906
- const { transaction_id: id, card: { token } = {} } = payment;
8133
+ // A card the shopper already has (a saved card, a wallet's payment method) gets
8134
+ // the intent and 3-D Secure of a new one; other cards are authorized with the order
8135
+ async _authorizePaymentMethod(token) {
8136
+ const cart = await this.getCart();
7907
8137
 
7908
- const intent = await this.updateIntent({
7909
- gateway: 'stripe',
7910
- intent: { id, payment_method: token },
7911
- });
8138
+ if (
8139
+ token.startsWith('pm_') &&
8140
+ isStripeChargeableAmount(cart.capture_total, cart.currency) &&
8141
+ this.params.createIntent !== false
8142
+ ) {
8143
+ await this.loadScripts(this.scripts);
7912
8144
 
7913
- if (intent.error) {
7914
- throw new Error(intent.error.message);
8145
+ const intent = await this._authorizeIntent(cart, token);
8146
+
8147
+ if (intent.id !== cart.billing?.intent?.stripe?.id) {
8148
+ await this.updateCart({
8149
+ billing: {
8150
+ method: 'card',
8151
+ intent: { stripe: cartIntent(cart, intent) },
8152
+ },
8153
+ });
8154
+ }
7915
8155
  }
7916
8156
 
7917
- await this.loadScripts(this.scripts);
8157
+ this.onSuccess();
8158
+ }
8159
+
8160
+ // Reuses the cart's intent while Stripe still has it for this card and amount
8161
+ async _authorizeIntent(cart, token) {
8162
+ const exIntent = cart.billing?.intent?.stripe;
8163
+ const intent =
8164
+ (exIntent?.client_secret &&
8165
+ (await this._retrieveIntent(cart, exIntent, token))) ||
8166
+ (await this._createIntent(cart, token));
7918
8167
 
8168
+ switch (intent.status) {
8169
+ case 'requires_capture':
8170
+ break;
8171
+
8172
+ case 'requires_confirmation':
8173
+ await this._confirmCardPayment(intent);
8174
+ break;
8175
+
8176
+ case 'requires_action':
8177
+ case 'requires_source_action': {
8178
+ const actionResult = await this.stripe.handleCardAction(
8179
+ intent.client_secret,
8180
+ );
8181
+
8182
+ if (actionResult.error) {
8183
+ throw new Error(actionResult.error.message);
8184
+ }
8185
+
8186
+ break;
8187
+ }
8188
+
8189
+ default:
8190
+ throw new Error(`Unsupported intent status: ${intent.status}`);
8191
+ }
8192
+
8193
+ return intent;
8194
+ }
8195
+
8196
+ async _retrieveIntent(cart, exIntent, token) {
8197
+ const { paymentIntent: intent, error } =
8198
+ await this.stripe.retrievePaymentIntent(exIntent.client_secret);
8199
+
8200
+ const matches =
8201
+ !error &&
8202
+ intent.amount === intentAmount(cart) &&
8203
+ intent.currency === intentCurrency(cart) &&
8204
+ intent.payment_method === token &&
8205
+ !['canceled', 'requires_source', 'requires_payment_method'].includes(
8206
+ intent.status,
8207
+ );
8208
+
8209
+ return matches ? intent : null;
8210
+ }
8211
+
8212
+ async authenticate(payment) {
8213
+ const { transaction_id: id, card: { token } = {} } = payment;
8214
+
8215
+ // The payment is re-queried whatever happens here: an intent the shopper
8216
+ // already confirmed can't be updated, and the payment must still learn it
7919
8217
  try {
7920
- const result = await this._confirmCardPayment(intent);
8218
+ const intent = await this.updateIntent({
8219
+ gateway: 'stripe',
8220
+ intent: { id, payment_method: token },
8221
+ });
7921
8222
 
7922
- return result;
8223
+ if (intent.error) {
8224
+ throw new Error(intent.error.message);
8225
+ }
8226
+
8227
+ await this.loadScripts(this.scripts);
8228
+
8229
+ return await this._confirmCardPayment(intent);
7923
8230
  } finally {
7924
8231
  await this.resetAsyncPayment(payment.id);
7925
8232
  }
7926
8233
  }
7927
8234
 
7928
- async _createIntent(cart, paymentMethod) {
7929
- const { account, currency, capture_total, auth_total } = cart;
7930
- const stripeCustomer = account && account.stripe_customer;
7931
- const stripeCurrency = (currency || 'USD').toLowerCase();
7932
- const amount = stripeAmountByCurrency(currency, capture_total + auth_total);
8235
+ async _createIntent(cart, token) {
8236
+ const stripeCustomer = cart.account && cart.account.stripe_customer;
7933
8237
  const intent = await this.createIntent({
7934
8238
  gateway: 'stripe',
7935
8239
  intent: {
7936
- amount,
7937
- currency: stripeCurrency,
7938
- payment_method: paymentMethod.token,
8240
+ amount: intentAmount(cart),
8241
+ currency: intentCurrency(cart),
8242
+ payment_method: token,
7939
8243
  capture_method: 'manual',
7940
8244
  setup_future_usage: 'off_session',
7941
8245
  ...(stripeCustomer ? { customer: stripeCustomer } : {}),
@@ -7946,20 +8250,6 @@
7946
8250
  throw new Error('Stripe payment intent is not defined');
7947
8251
  }
7948
8252
 
7949
- switch (intent.status) {
7950
- case 'requires_capture':
7951
- case 'requires_confirmation':
7952
- break;
7953
-
7954
- default:
7955
- throw new Error(`Unsupported intent status: ${intent.status}`);
7956
- }
7957
-
7958
- // Confirm the payment intent
7959
- if (intent.status === 'requires_confirmation') {
7960
- await this._confirmCardPayment(intent);
7961
- }
7962
-
7963
8253
  return intent;
7964
8254
  }
7965
8255
 
@@ -8027,24 +8317,58 @@
8027
8317
  StripeIDealPayment.stripeElement = stripeElement;
8028
8318
  }
8029
8319
 
8030
- async createElements() {
8320
+ /** @returns {import('@stripe/stripe-js').StripeElements} */
8321
+ get stripeElements() {
8322
+ return StripeIDealPayment.stripeElements;
8323
+ }
8324
+
8325
+ /** @param {import('@stripe/stripe-js').StripeElements} stripeElements */
8326
+ set stripeElements(stripeElements) {
8327
+ StripeIDealPayment.stripeElements = stripeElements;
8328
+ }
8329
+
8330
+ /** @param {import('../../../types').Cart} cart */
8331
+ async createElements(cart) {
8031
8332
  await this.loadScripts(this.scripts);
8032
8333
 
8334
+ if (this.params.paymentElement) {
8335
+ this.stripeElements = createPaymentElements(
8336
+ this.stripe,
8337
+ getIDealElementsOptions(cart),
8338
+ this.params,
8339
+ );
8340
+
8341
+ return;
8342
+ }
8343
+
8033
8344
  const elements = this.stripe.elements(this.params.config);
8034
8345
 
8035
8346
  this.stripeElement = createElement('idealBank', elements, this.params);
8036
8347
  }
8037
8348
 
8038
8349
  async tokenize() {
8039
- if (!this.stripeElement) {
8350
+ const { paymentElement } = this.params;
8351
+
8352
+ if (paymentElement && !this.stripeElements) {
8040
8353
  throw new Error('Stripe payment element is not defined');
8041
8354
  }
8042
8355
 
8043
8356
  await this.loadScripts(this.scripts);
8044
8357
 
8045
8358
  const cart = await this.getCart();
8046
- const { paymentMethod, error: paymentMethodError } =
8047
- await createIDealPaymentMethod(this.stripe, this.stripeElement, cart);
8359
+ const { paymentMethod, error: paymentMethodError } = paymentElement
8360
+ ? await createPaymentMethodFromElements(
8361
+ this.stripe,
8362
+ this.stripeElements,
8363
+ getIDealElementsOptions(cart),
8364
+ cart,
8365
+ )
8366
+ : // without an idealBank element the shopper picks the bank on the iDEAL page
8367
+ await createIDealPaymentMethod(
8368
+ this.stripe,
8369
+ this.stripeElement || {},
8370
+ cart,
8371
+ );
8048
8372
 
8049
8373
  if (paymentMethodError) {
8050
8374
  throw new Error(paymentMethodError.message);
@@ -8055,6 +8379,38 @@
8055
8379
  await this.stripe.handleCardAction(intent.client_secret);
8056
8380
  }
8057
8381
 
8382
+ async handleRedirect(queryParams) {
8383
+ const { redirect_status, payment_intent_client_secret } = queryParams;
8384
+
8385
+ if (redirect_status !== 'succeeded') {
8386
+ throw new UnableAuthenticatePaymentMethodError();
8387
+ }
8388
+
8389
+ await this.loadScripts(this.scripts);
8390
+
8391
+ const { paymentIntent, error } = await this.stripe.retrievePaymentIntent(
8392
+ payment_intent_client_secret,
8393
+ );
8394
+
8395
+ if (error) {
8396
+ throw new Error(error.message);
8397
+ }
8398
+
8399
+ if (
8400
+ !['succeeded', 'requires_capture', 'processing'].includes(
8401
+ paymentIntent.status,
8402
+ )
8403
+ ) {
8404
+ throw new Error(
8405
+ paymentIntent.last_payment_error?.message ||
8406
+ `Unsupported intent status (${paymentIntent.status})`,
8407
+ );
8408
+ }
8409
+
8410
+ // the cart's billing was written in `tokenize`
8411
+ this.onSuccess();
8412
+ }
8413
+
8058
8414
  /**
8059
8415
  * @param {object} cart
8060
8416
  * @param {import('@stripe/stripe-js').PaymentMethod} paymentMethod
@@ -8072,7 +8428,8 @@
8072
8428
  payment_method_types: 'ideal',
8073
8429
  confirmation_method: 'manual',
8074
8430
  confirm: true,
8075
- return_url: window.location.href,
8431
+ // `gateway` lets `handleRedirect` run on return; Stripe appends the intent and `redirect_status`
8432
+ return_url: getReturnUrl(),
8076
8433
  },
8077
8434
  });
8078
8435
 
@@ -8104,6 +8461,14 @@
8104
8461
  }
8105
8462
  }
8106
8463
 
8464
+ function getReturnUrl() {
8465
+ const url = new URL(window.location.href);
8466
+
8467
+ url.searchParams.set('gateway', 'stripe');
8468
+
8469
+ return url.toString();
8470
+ }
8471
+
8107
8472
  /** @typedef {import('@stripe/stripe-js').Stripe} Stripe */
8108
8473
 
8109
8474
  class StripeBancontactPayment extends Payment {
@@ -8423,6 +8788,7 @@
8423
8788
  async _onPaymentMethod(event) {
8424
8789
  const {
8425
8790
  payerEmail,
8791
+ payerName,
8426
8792
  paymentMethod: { id: paymentMethod, card, billing_details },
8427
8793
  shippingAddress,
8428
8794
  shippingOption,
@@ -8441,6 +8807,8 @@
8441
8807
  }),
8442
8808
  billing: {
8443
8809
  ...this._mapBillingAddress(billing_details),
8810
+ // a wallet may leave the card's address without a name; the payer's, when asked for, fills it
8811
+ name: billing_details?.name || payerName,
8444
8812
  method: 'card',
8445
8813
  card: {
8446
8814
  gateway: 'stripe',
@@ -8679,6 +9047,7 @@
8679
9047
  async _onPaymentMethod(event) {
8680
9048
  const {
8681
9049
  payerEmail,
9050
+ payerName,
8682
9051
  paymentMethod: { id: paymentMethod, card, billing_details },
8683
9052
  shippingAddress,
8684
9053
  shippingOption,
@@ -8697,6 +9066,8 @@
8697
9066
  }),
8698
9067
  billing: {
8699
9068
  ...this._mapBillingAddress(billing_details),
9069
+ // a wallet may leave the card's address without a name; the payer's, when asked for, fills it
9070
+ name: billing_details?.name || payerName,
8700
9071
  method: 'card',
8701
9072
  card: {
8702
9073
  gateway: 'stripe',
@@ -11127,6 +11498,238 @@
11127
11498
  }
11128
11499
  }
11129
11500
 
11501
+ // Saferpay payment-page brands per Swell method id; a list is joined because the
11502
+ // vault rejects arrays in `PaymentMethods`
11503
+ const PAYMENT_METHODS = {
11504
+ card: [
11505
+ 'AMEX',
11506
+ 'DINERS',
11507
+ 'MAESTRO',
11508
+ 'JCB',
11509
+ 'VISA',
11510
+ 'BANCONTACT',
11511
+ 'MASTERCARD',
11512
+ 'SAFERPAYTEST',
11513
+ 'UNIONPAY',
11514
+ ].join(','),
11515
+ twint: 'TWINT',
11516
+ };
11517
+
11518
+ /**
11519
+ * @param {string} method 'card' or 'twint'
11520
+ * @returns {string}
11521
+ */
11522
+ function getSaferpayPaymentMethods(method) {
11523
+ return PAYMENT_METHODS[method];
11524
+ }
11525
+
11526
+ /**
11527
+ * The Saferpay payment page request for a cart.
11528
+ *
11529
+ * @param {object} cart
11530
+ * @param {string} method 'card' or 'twint'
11531
+ * @returns {object}
11532
+ */
11533
+ function getSaferpayPaymentPageData(cart, method) {
11534
+ const currency = (cart.currency || 'USD').toUpperCase();
11535
+ const returnUrl = window.location.origin + window.location.pathname;
11536
+
11537
+ return {
11538
+ Payment: {
11539
+ Amount: {
11540
+ Value: amountInCents(currency, cart.grand_total || 0),
11541
+ CurrencyCode: currency,
11542
+ },
11543
+ Description: 'Saferpay payment powered by Swell',
11544
+ },
11545
+ PaymentMethods: getSaferpayPaymentMethods(method),
11546
+ ReturnUrls: {
11547
+ Success: `${returnUrl}?gateway=saferpay&redirect_status=succeeded`,
11548
+ Fail: `${returnUrl}?gateway=saferpay&redirect_status=failed`,
11549
+ Abort: `${returnUrl}?gateway=saferpay&redirect_status=canceled`,
11550
+ },
11551
+ };
11552
+ }
11553
+
11554
+ /**
11555
+ * @param {object} state the Saferpay transaction state
11556
+ * @returns {boolean}
11557
+ */
11558
+ function isSaferpayCardPayment(state) {
11559
+ const brand = state?.PaymentMeans?.Brand;
11560
+
11561
+ return Boolean(
11562
+ state?.RegistrationResult?.Success &&
11563
+ brand &&
11564
+ PAYMENT_METHODS.card.split(',').includes(brand.PaymentMethod),
11565
+ );
11566
+ }
11567
+
11568
+ /**
11569
+ * @param {object} state the Saferpay transaction state
11570
+ * @returns {boolean}
11571
+ */
11572
+ function isSaferpayTwintPayment(state) {
11573
+ return state?.PaymentMeans?.Brand?.PaymentMethod === 'TWINT';
11574
+ }
11575
+
11576
+ /**
11577
+ * The card stored on the cart's billing from a Saferpay card state.
11578
+ *
11579
+ * @param {object} state
11580
+ * @returns {object}
11581
+ */
11582
+ function getSaferpayCard(state) {
11583
+ const {
11584
+ RegistrationResult: {
11585
+ Alias: { Id: token },
11586
+ },
11587
+ PaymentMeans: {
11588
+ Card: { ExpMonth: exp_month, ExpYear: exp_year, MaskedNumber },
11589
+ Brand: { Name: brand },
11590
+ },
11591
+ } = state;
11592
+
11593
+ return {
11594
+ token,
11595
+ exp_month,
11596
+ exp_year,
11597
+ brand,
11598
+ last4: MaskedNumber.slice(-4),
11599
+ gateway: 'saferpay',
11600
+ };
11601
+ }
11602
+
11603
+ /**
11604
+ * @param {object} state
11605
+ * @returns {string | undefined}
11606
+ */
11607
+ function getSaferpayTransactionId(state) {
11608
+ return state?.Transaction?.Id;
11609
+ }
11610
+
11611
+ /**
11612
+ * Saferpay payment page: `tokenize` sends the shopper to Saferpay, and
11613
+ * `handleRedirect` turns the returned transaction into the cart's billing.
11614
+ * The card and TWINT classes share it; each one handles only the redirect of
11615
+ * the method the cart was tokenized with, so a host can pass both.
11616
+ */
11617
+ class SaferpayPayment extends Payment {
11618
+ constructor(api, options, params, methods, methodId) {
11619
+ super(api, options, params, methods[methodId]);
11620
+ this.methodId = methodId;
11621
+ this.cardMethod = methods.card;
11622
+ }
11623
+
11624
+ async tokenize() {
11625
+ const cart = await this.getCart();
11626
+
11627
+ // a card Saferpay already registered on this cart pays again without a new page visit
11628
+ if (
11629
+ this.methodId === 'card' &&
11630
+ cart.billing?.card?.gateway === 'saferpay' &&
11631
+ !this.params?.replace
11632
+ ) {
11633
+ return this.onSuccess();
11634
+ }
11635
+
11636
+ const intent = await this.createIntent({
11637
+ gateway: 'saferpay',
11638
+ intent: getSaferpayPaymentPageData(cart, this.methodId),
11639
+ });
11640
+
11641
+ if (!intent) {
11642
+ throw new Error('Saferpay payment page is not defined');
11643
+ }
11644
+
11645
+ await this.updateCart({
11646
+ billing: {
11647
+ method: this.methodId,
11648
+ intent: {
11649
+ saferpay: {
11650
+ token: intent.token,
11651
+ },
11652
+ },
11653
+ },
11654
+ });
11655
+
11656
+ window.location.replace(intent.redirect_url);
11657
+ }
11658
+
11659
+ async handleRedirect(queryParams) {
11660
+ const { redirect_status: status } = queryParams;
11661
+ const cart = await this.getCart();
11662
+
11663
+ // another Saferpay method of this host was tokenized: its instance answers
11664
+ if (cart.billing?.method !== this.methodId) {
11665
+ return;
11666
+ }
11667
+
11668
+ switch (status) {
11669
+ case 'succeeded':
11670
+ return this._handleSuccessfulRedirect(cart);
11671
+ case 'canceled':
11672
+ return this.onCancel();
11673
+ case 'failed':
11674
+ throw new UnableAuthenticatePaymentMethodError();
11675
+ default:
11676
+ throw new Error(`Unknown redirect status: ${status}.`);
11677
+ }
11678
+ }
11679
+
11680
+ async _handleSuccessfulRedirect(cart) {
11681
+ const token = cart.billing?.intent?.saferpay?.token;
11682
+
11683
+ if (!token) {
11684
+ throw new Error('Saferpay transaction token is not defined');
11685
+ }
11686
+
11687
+ const intent = await this.updateIntent({
11688
+ gateway: 'saferpay',
11689
+ intent: { token },
11690
+ });
11691
+
11692
+ if (!intent) {
11693
+ throw new Error('Saferpay transaction is not defined');
11694
+ }
11695
+
11696
+ const { state } = intent;
11697
+ const transaction = { transaction_id: getSaferpayTransactionId(state) };
11698
+
11699
+ if (isSaferpayCardPayment(state)) {
11700
+ const card = getSaferpayCard(state);
11701
+
11702
+ if (!isLiveMode(this.cardMethod?.mode)) {
11703
+ card.test = true;
11704
+ }
11705
+
11706
+ await this.updateCart({
11707
+ billing: { method: 'card', card, intent: { saferpay: transaction } },
11708
+ });
11709
+ } else if (isSaferpayTwintPayment(state)) {
11710
+ await this.updateCart({
11711
+ billing: { method: 'twint', intent: { saferpay: transaction } },
11712
+ });
11713
+ } else {
11714
+ throw new Error('Payment method not supported by this gateway.');
11715
+ }
11716
+
11717
+ this.onSuccess();
11718
+ }
11719
+ }
11720
+
11721
+ class SaferpayCardPayment extends SaferpayPayment {
11722
+ constructor(api, options, params, methods) {
11723
+ super(api, options, params, methods, 'card');
11724
+ }
11725
+ }
11726
+
11727
+ class SaferpayTwintPayment extends SaferpayPayment {
11728
+ constructor(api, options, params, methods) {
11729
+ super(api, options, params, methods, 'twint');
11730
+ }
11731
+ }
11732
+
11130
11733
  let convesioSettings = null;
11131
11734
 
11132
11735
  function getBaseConvesioApiUrl(mode) {
@@ -12807,6 +13410,33 @@
12807
13410
  return this.api.request('get', '/payments', id);
12808
13411
  }
12809
13412
 
13413
+ /**
13414
+ * Sends the shopper to the gateway when a submitted order waits for an
13415
+ * asynchronous payment; resolves `true` after starting the redirect.
13416
+ *
13417
+ * @param {object} order the result of `cart.submitOrder()`
13418
+ * @returns {Promise<boolean>}
13419
+ */
13420
+ async handleAsyncRedirect(order) {
13421
+ const paymentId =
13422
+ order?.authorized_payment_id ?? order?.authorizedPaymentId;
13423
+
13424
+ if (order?.status !== 'payment_pending' || !paymentId) {
13425
+ return false;
13426
+ }
13427
+
13428
+ const payment = await this.get(paymentId);
13429
+ const url = payment?.async_data?.url ?? payment?.asyncData?.url;
13430
+
13431
+ if (!payment || payment.captured || !url) {
13432
+ return false;
13433
+ }
13434
+
13435
+ window.location.replace(url);
13436
+
13437
+ return true;
13438
+ }
13439
+
12810
13440
  async methods() {
12811
13441
  if (this.methodSettings) {
12812
13442
  return this.methodSettings;
@@ -12824,7 +13454,7 @@
12824
13454
  throw new Error('Payment element parameters are not provided');
12825
13455
  }
12826
13456
 
12827
- const paymentInstances = await this._createPaymentInstances();
13457
+ const paymentInstances = await this._createPaymentInstances(params);
12828
13458
  const cart = await this.payment.getCart();
12829
13459
 
12830
13460
  await this._performPaymentAction(
@@ -12843,7 +13473,7 @@
12843
13473
  throw new Error('Tokenization parameters are not provided');
12844
13474
  }
12845
13475
 
12846
- const paymentInstances = await this._createPaymentInstances();
13476
+ const paymentInstances = await this._createPaymentInstances(params);
12847
13477
 
12848
13478
  await this._performPaymentAction(paymentInstances, 'tokenize');
12849
13479
  }
@@ -12863,7 +13493,7 @@
12863
13493
 
12864
13494
  removeUrlParams();
12865
13495
 
12866
- const paymentInstances = await this._createPaymentInstances();
13496
+ const paymentInstances = await this._createPaymentInstances(params);
12867
13497
 
12868
13498
  await this._performPaymentAction(
12869
13499
  paymentInstances,
@@ -12956,9 +13586,10 @@
12956
13586
  /**
12957
13587
  * @returns {Promise<Payment[]>}
12958
13588
  */
12959
- async _createPaymentInstances() {
13589
+ // Takes the caller's params: this.params can change while the methods load (two createElements at once)
13590
+ async _createPaymentInstances(callParams) {
12960
13591
  const paymentMethods = await this._getPaymentMethods();
12961
- const params = adjustParams(this.params);
13592
+ const params = adjustParams(callParams);
12962
13593
 
12963
13594
  return Object.entries(params).reduce((acc, [method, params]) => {
12964
13595
  const methodSettings = paymentMethods[method];
@@ -13056,6 +13687,8 @@
13056
13687
  return this._getAmazonPaymentClass(gateway);
13057
13688
  case 'sezzle':
13058
13689
  return this._getSezzlePaymentClass(gateway);
13690
+ case 'twint':
13691
+ return this._getTwintPaymentClass(gateway);
13059
13692
  default:
13060
13693
  return null;
13061
13694
  }
@@ -13069,6 +13702,17 @@
13069
13702
  return QuickpayCardPayment;
13070
13703
  case 'convesiopay':
13071
13704
  return ConvesioCardPayment;
13705
+ case 'saferpay':
13706
+ return SaferpayCardPayment;
13707
+ default:
13708
+ return null;
13709
+ }
13710
+ }
13711
+
13712
+ _getTwintPaymentClass(gateway) {
13713
+ switch (gateway) {
13714
+ case 'saferpay':
13715
+ return SaferpayTwintPayment;
13072
13716
  default:
13073
13717
  return null;
13074
13718
  }
@@ -13620,7 +14264,7 @@
13620
14264
  const api = {};
13621
14265
 
13622
14266
  Object.assign(api, {
13623
- version: '5.9.4',
14267
+ version: '5.10.0',
13624
14268
  options,
13625
14269
  request,
13626
14270