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.
package/dist/swell.cjs CHANGED
@@ -6150,6 +6150,10 @@ class AppComponent {
6150
6150
  }
6151
6151
  }
6152
6152
 
6153
+ /**
6154
+ * @deprecated Payment components are mounted by `@swell/apps-sdk/components`
6155
+ * at the host place `checkout.payment.methods`.
6156
+ */
6153
6157
  class AppPaymentComponent extends AppComponent {
6154
6158
  constructor(app, component, api, options) {
6155
6159
  super(app, component, api, options);
@@ -6199,6 +6203,7 @@ class AppPaymentComponent extends AppComponent {
6199
6203
  }
6200
6204
  }
6201
6205
 
6206
+ /** @deprecated Use `@swell/apps-sdk/components` instead. */
6202
6207
  class App {
6203
6208
  constructor(app, api, options) {
6204
6209
  this.app = app;
@@ -6307,6 +6312,13 @@ class App {
6307
6312
  }
6308
6313
  }
6309
6314
 
6315
+ let deprecationWarned = false;
6316
+
6317
+ /**
6318
+ * @deprecated App components are rendered by `@swell/apps-sdk/components`
6319
+ * (`createComponents().mount()`); this module will be removed in the next
6320
+ * major version.
6321
+ */
6310
6322
  class AppController {
6311
6323
  #apps = new Map();
6312
6324
 
@@ -6315,7 +6327,15 @@ class AppController {
6315
6327
  this.options = options;
6316
6328
  }
6317
6329
 
6330
+ /** @deprecated Use `@swell/apps-sdk/components` instead. */
6318
6331
  async load(appId) {
6332
+ if (!deprecationWarned) {
6333
+ deprecationWarned = true;
6334
+ console.warn(
6335
+ 'swell.app is deprecated: render app components with @swell/apps-sdk/components',
6336
+ );
6337
+ }
6338
+
6319
6339
  const loadedApp = this.#apps.get(appId);
6320
6340
 
6321
6341
  if (loadedApp) {
@@ -7440,25 +7460,184 @@ async function createPaymentMethod(stripe, cardElement, cart) {
7440
7460
  billing_details: billingDetails,
7441
7461
  });
7442
7462
 
7443
- return error
7444
- ? { error }
7445
- : {
7446
- gateway: 'stripe',
7447
- token: paymentMethod.id,
7448
- last4: paymentMethod.card.last4,
7449
- exp_month: paymentMethod.card.exp_month,
7450
- exp_year: paymentMethod.card.exp_year,
7451
- brand: paymentMethod.card.brand,
7452
- display_brand: paymentMethod.card.display_brand,
7453
- address_check: paymentMethod.card.checks.address_line1_check,
7454
- cvc_check: paymentMethod.card.checks.cvc_check,
7455
- zip_check: paymentMethod.card.checks.address_postal_code_check,
7456
- };
7463
+ return error ? { error } : mapCardPaymentMethod(paymentMethod);
7464
+ }
7465
+
7466
+ /**
7467
+ * Elements group options of the card Payment Element for the cart amount,
7468
+ * matching the intent StripeCardPayment#_createIntent creates.
7469
+ *
7470
+ * @param {Cart} cart
7471
+ * @returns {import('@stripe/stripe-js').StripeElementsOptionsMode}
7472
+ */
7473
+ function getCardElementsOptions(cart) {
7474
+ const { capture_total, auth_total } = cart;
7475
+ const currency = cart.currency || 'USD';
7476
+ const stripeCurrency = currency.toLowerCase();
7477
+
7478
+ // the card is only stored when the amount is not chargeable, no intent is created
7479
+ if (!isStripeChargeableAmount(capture_total, cart.currency)) {
7480
+ return {
7481
+ mode: 'setup',
7482
+ currency: stripeCurrency,
7483
+ paymentMethodTypes: ['card'],
7484
+ };
7485
+ }
7486
+
7487
+ return {
7488
+ mode: 'payment',
7489
+ amount: stripeAmountByCurrency(currency, capture_total + (auth_total || 0)),
7490
+ currency: stripeCurrency,
7491
+ captureMethod: 'manual',
7492
+ setupFutureUsage: 'off_session',
7493
+ paymentMethodTypes: ['card'],
7494
+ };
7495
+ }
7496
+
7497
+ /**
7498
+ * Elements group options of the iDEAL Payment Element,
7499
+ * matching the intent StripeIDealPayment#_createIntent creates.
7500
+ *
7501
+ * @param {Cart} cart
7502
+ * @returns {import('@stripe/stripe-js').StripeElementsOptionsMode}
7503
+ */
7504
+ function getIDealElementsOptions(cart) {
7505
+ const { capture_total } = cart;
7506
+ const currency = cart.currency || 'EUR';
7507
+
7508
+ return {
7509
+ mode: 'payment',
7510
+ amount: stripeAmountByCurrency(currency, capture_total),
7511
+ currency: currency.toLowerCase(),
7512
+ paymentMethodTypes: ['ideal'],
7513
+ };
7514
+ }
7515
+
7516
+ /**
7517
+ * Creates an Elements group without an intent and mounts the Payment Element
7518
+ * into `#${params.elementId || 'payment-element'}`.
7519
+ *
7520
+ * @param {Stripe} stripe
7521
+ * @param {import('@stripe/stripe-js').StripeElementsOptionsMode} elementsOptions
7522
+ * @param {object} params
7523
+ * @returns {import('@stripe/stripe-js').StripeElements}
7524
+ */
7525
+ function createPaymentElements(stripe, elementsOptions, params) {
7526
+ const elements = stripe.elements({
7527
+ ...params.config,
7528
+ ...elementsOptions,
7529
+ // lets stripe.createPaymentMethod read the Payment Element
7530
+ paymentMethodCreation: 'manual',
7531
+ ...(params.appearance && { appearance: params.appearance }),
7532
+ });
7533
+
7534
+ createElement('payment', elements, {
7535
+ ...params,
7536
+ options: {
7537
+ layout: 'tabs',
7538
+ fields: { billingDetails: 'never' },
7539
+ // wallets are separate express buttons
7540
+ wallets: { applePay: 'never', googlePay: 'never' },
7541
+ ...params.options,
7542
+ },
7543
+ });
7544
+
7545
+ return elements;
7546
+ }
7547
+
7548
+ /**
7549
+ * Creates a PaymentMethod from the Payment Element.
7550
+ * Brings the Elements amount up to date with the cart first.
7551
+ *
7552
+ * @param {Stripe} stripe
7553
+ * @param {import('@stripe/stripe-js').StripeElements} elements
7554
+ * @param {import('@stripe/stripe-js').StripeElementsOptionsMode} elementsOptions
7555
+ * @param {Cart} cart
7556
+ * @returns {Promise<import('@stripe/stripe-js').PaymentMethodResult>}
7557
+ */
7558
+ async function createPaymentMethodFromElements(
7559
+ stripe,
7560
+ elements,
7561
+ elementsOptions,
7562
+ cart,
7563
+ ) {
7564
+ // setup mode stores the card without an intent, there is no amount to update
7565
+ if (elementsOptions.mode === 'payment') {
7566
+ await elements.update(elementsOptions);
7567
+ }
7568
+
7569
+ const { error } = await elements.submit();
7570
+
7571
+ if (error) {
7572
+ return { error };
7573
+ }
7574
+
7575
+ const { address, ...details } = getBillingDetails(cart);
7576
+
7577
+ // fields the Payment Element does not collect must all be passed, null when unknown
7578
+ return stripe.createPaymentMethod({
7579
+ elements,
7580
+ params: {
7581
+ billing_details: {
7582
+ name: null,
7583
+ email: null,
7584
+ phone: null,
7585
+ ...details,
7586
+ address: {
7587
+ line1: null,
7588
+ line2: null,
7589
+ city: null,
7590
+ state: null,
7591
+ postal_code: null,
7592
+ country: null,
7593
+ ...address,
7594
+ },
7595
+ },
7596
+ },
7597
+ });
7598
+ }
7599
+
7600
+ /**
7601
+ * @param {Stripe} stripe
7602
+ * @param {import('@stripe/stripe-js').StripeElements} elements
7603
+ * @param {Cart} cart
7604
+ */
7605
+ async function createCardPaymentMethodFromElements(
7606
+ stripe,
7607
+ elements,
7608
+ cart,
7609
+ ) {
7610
+ const { paymentMethod, error } = await createPaymentMethodFromElements(
7611
+ stripe,
7612
+ elements,
7613
+ getCardElementsOptions(cart),
7614
+ cart,
7615
+ );
7616
+
7617
+ return error ? { error } : mapCardPaymentMethod(paymentMethod);
7618
+ }
7619
+
7620
+ /**
7621
+ * @param {import('@stripe/stripe-js').PaymentMethod} paymentMethod
7622
+ */
7623
+ function mapCardPaymentMethod(paymentMethod) {
7624
+ return {
7625
+ gateway: 'stripe',
7626
+ token: paymentMethod.id,
7627
+ last4: paymentMethod.card.last4,
7628
+ exp_month: paymentMethod.card.exp_month,
7629
+ exp_year: paymentMethod.card.exp_year,
7630
+ brand: paymentMethod.card.brand,
7631
+ display_brand: paymentMethod.card.display_brand,
7632
+ address_check: paymentMethod.card.checks.address_line1_check,
7633
+ cvc_check: paymentMethod.card.checks.cvc_check,
7634
+ zip_check: paymentMethod.card.checks.address_postal_code_check,
7635
+ };
7457
7636
  }
7458
7637
 
7459
7638
  /**
7460
7639
  * @param {Stripe} stripe
7461
- * @param {import('@stripe/stripe-js').StripeIdealBankElement} element
7640
+ * @param {import('@stripe/stripe-js').StripeIdealBankElement | {}} element `{}` when there is no idealBank element
7462
7641
  * @param {Cart} cart
7463
7642
  */
7464
7643
  async function createIDealPaymentMethod(stripe, element, cart) {
@@ -7646,6 +7825,29 @@ function isStripeChargeableAmount(amount, currency) {
7646
7825
  return !minAmount || amount >= minAmount;
7647
7826
  }
7648
7827
 
7828
+ // The cart keeps what checkout v1 keeps, so the next tokenize can retrieve and reuse
7829
+ // the intent; the vault cancels the replaced ones when the order is paid
7830
+ const cartIntent = (cart, intent) => {
7831
+ const exIntent = cart.billing?.intent?.stripe;
7832
+
7833
+ return {
7834
+ id: intent.id,
7835
+ client_secret: intent.client_secret,
7836
+ amount: intent.amount,
7837
+ currency: intent.currency,
7838
+ ...(Boolean(cart.auth_total) && { auth_amount: cart.auth_total }),
7839
+ ...(exIntent?.id &&
7840
+ exIntent.id !== intent.id && {
7841
+ canceled: [...(exIntent.canceled || []), exIntent.id],
7842
+ }),
7843
+ };
7844
+ };
7845
+
7846
+ const intentAmount = ({ currency, capture_total, auth_total }) =>
7847
+ stripeAmountByCurrency(currency, capture_total + auth_total);
7848
+
7849
+ const intentCurrency = ({ currency }) => (currency || 'USD').toLowerCase();
7850
+
7649
7851
  /** @typedef {import('@stripe/stripe-js').Stripe} Stripe */
7650
7852
  /** @typedef {import('@stripe/stripe-js').StripeCardElement} StripeCardElement */
7651
7853
  /** @typedef {import('@stripe/stripe-js').StripeCardNumberElement} StripeCardNumberElement */
@@ -7689,9 +7891,30 @@ class StripeCardPayment extends Payment {
7689
7891
  StripeCardPayment.stripeElement = stripeElement;
7690
7892
  }
7691
7893
 
7692
- async createElements() {
7894
+ /** @returns {import('@stripe/stripe-js').StripeElements} */
7895
+ get stripeElements() {
7896
+ return StripeCardPayment.stripeElements;
7897
+ }
7898
+
7899
+ /** @param {import('@stripe/stripe-js').StripeElements} stripeElements */
7900
+ set stripeElements(stripeElements) {
7901
+ StripeCardPayment.stripeElements = stripeElements;
7902
+ }
7903
+
7904
+ /** @param {import('../../../types').Cart} cart */
7905
+ async createElements(cart) {
7693
7906
  await this.loadScripts(this.scripts);
7694
7907
 
7908
+ if (this.params.paymentElement) {
7909
+ this.stripeElements = createPaymentElements(
7910
+ this.stripe,
7911
+ getCardElementsOptions(cart),
7912
+ this.params,
7913
+ );
7914
+
7915
+ return;
7916
+ }
7917
+
7695
7918
  const elements = this.stripe.elements(this.params.config);
7696
7919
 
7697
7920
  if (this.params.separateElements) {
@@ -7704,18 +7927,26 @@ class StripeCardPayment extends Payment {
7704
7927
  }
7705
7928
 
7706
7929
  async tokenize() {
7707
- if (!this.stripeElement) {
7930
+ const { paymentElement, paymentMethod: token } = this.params;
7931
+
7932
+ if (token) {
7933
+ return this._authorizePaymentMethod(token);
7934
+ }
7935
+
7936
+ if (!(paymentElement ? this.stripeElements : this.stripeElement)) {
7708
7937
  throw new Error('Stripe payment element is not defined');
7709
7938
  }
7710
7939
 
7711
7940
  await this.loadScripts(this.scripts);
7712
7941
 
7713
7942
  const cart = await this.getCart();
7714
- const paymentMethod = await createPaymentMethod(
7715
- this.stripe,
7716
- this.stripeElement,
7717
- cart,
7718
- );
7943
+ const paymentMethod = paymentElement
7944
+ ? await createCardPaymentMethodFromElements(
7945
+ this.stripe,
7946
+ this.stripeElements,
7947
+ cart,
7948
+ )
7949
+ : await createPaymentMethod(this.stripe, this.stripeElement, cart);
7719
7950
 
7720
7951
  if (paymentMethod.error) {
7721
7952
  throw new Error(paymentMethod.error.message);
@@ -7725,8 +7956,12 @@ class StripeCardPayment extends Payment {
7725
7956
  paymentMethod.test = true;
7726
7957
  }
7727
7958
 
7728
- // should save payment method data when payment amount is not chargeable
7729
- if (!isStripeChargeableAmount(cart.capture_total, cart.currency)) {
7959
+ // should save payment method data when payment amount is not chargeable,
7960
+ // or when the host collects the card without authorizing it now
7961
+ if (
7962
+ !isStripeChargeableAmount(cart.capture_total, cart.currency) ||
7963
+ this.params.createIntent === false
7964
+ ) {
7730
7965
  await this.updateCart({
7731
7966
  billing: {
7732
7967
  method: 'card',
@@ -7737,60 +7972,129 @@ class StripeCardPayment extends Payment {
7737
7972
  return this.onSuccess();
7738
7973
  }
7739
7974
 
7740
- const intent = await this._createIntent(cart, paymentMethod);
7975
+ const intent = await this._authorizeIntent(cart, paymentMethod.token);
7741
7976
 
7742
7977
  await this.updateCart({
7743
7978
  billing: {
7744
7979
  method: 'card',
7745
7980
  card: paymentMethod,
7746
- intent: {
7747
- stripe: {
7748
- id: intent.id,
7749
- ...(Boolean(cart.auth_total) && {
7750
- auth_amount: cart.auth_total,
7751
- }),
7752
- },
7753
- },
7981
+ intent: { stripe: cartIntent(cart, intent) },
7754
7982
  },
7755
7983
  });
7756
7984
 
7757
7985
  this.onSuccess();
7758
7986
  }
7759
7987
 
7760
- async authenticate(payment) {
7761
- const { transaction_id: id, card: { token } = {} } = payment;
7988
+ // A card the shopper already has (a saved card, a wallet's payment method) gets
7989
+ // the intent and 3-D Secure of a new one; other cards are authorized with the order
7990
+ async _authorizePaymentMethod(token) {
7991
+ const cart = await this.getCart();
7762
7992
 
7763
- const intent = await this.updateIntent({
7764
- gateway: 'stripe',
7765
- intent: { id, payment_method: token },
7766
- });
7993
+ if (
7994
+ token.startsWith('pm_') &&
7995
+ isStripeChargeableAmount(cart.capture_total, cart.currency) &&
7996
+ this.params.createIntent !== false
7997
+ ) {
7998
+ await this.loadScripts(this.scripts);
7767
7999
 
7768
- if (intent.error) {
7769
- throw new Error(intent.error.message);
8000
+ const intent = await this._authorizeIntent(cart, token);
8001
+
8002
+ if (intent.id !== cart.billing?.intent?.stripe?.id) {
8003
+ await this.updateCart({
8004
+ billing: {
8005
+ method: 'card',
8006
+ intent: { stripe: cartIntent(cart, intent) },
8007
+ },
8008
+ });
8009
+ }
7770
8010
  }
7771
8011
 
7772
- await this.loadScripts(this.scripts);
8012
+ this.onSuccess();
8013
+ }
8014
+
8015
+ // Reuses the cart's intent while Stripe still has it for this card and amount
8016
+ async _authorizeIntent(cart, token) {
8017
+ const exIntent = cart.billing?.intent?.stripe;
8018
+ const intent =
8019
+ (exIntent?.client_secret &&
8020
+ (await this._retrieveIntent(cart, exIntent, token))) ||
8021
+ (await this._createIntent(cart, token));
7773
8022
 
8023
+ switch (intent.status) {
8024
+ case 'requires_capture':
8025
+ break;
8026
+
8027
+ case 'requires_confirmation':
8028
+ await this._confirmCardPayment(intent);
8029
+ break;
8030
+
8031
+ case 'requires_action':
8032
+ case 'requires_source_action': {
8033
+ const actionResult = await this.stripe.handleCardAction(
8034
+ intent.client_secret,
8035
+ );
8036
+
8037
+ if (actionResult.error) {
8038
+ throw new Error(actionResult.error.message);
8039
+ }
8040
+
8041
+ break;
8042
+ }
8043
+
8044
+ default:
8045
+ throw new Error(`Unsupported intent status: ${intent.status}`);
8046
+ }
8047
+
8048
+ return intent;
8049
+ }
8050
+
8051
+ async _retrieveIntent(cart, exIntent, token) {
8052
+ const { paymentIntent: intent, error } =
8053
+ await this.stripe.retrievePaymentIntent(exIntent.client_secret);
8054
+
8055
+ const matches =
8056
+ !error &&
8057
+ intent.amount === intentAmount(cart) &&
8058
+ intent.currency === intentCurrency(cart) &&
8059
+ intent.payment_method === token &&
8060
+ !['canceled', 'requires_source', 'requires_payment_method'].includes(
8061
+ intent.status,
8062
+ );
8063
+
8064
+ return matches ? intent : null;
8065
+ }
8066
+
8067
+ async authenticate(payment) {
8068
+ const { transaction_id: id, card: { token } = {} } = payment;
8069
+
8070
+ // The payment is re-queried whatever happens here: an intent the shopper
8071
+ // already confirmed can't be updated, and the payment must still learn it
7774
8072
  try {
7775
- const result = await this._confirmCardPayment(intent);
8073
+ const intent = await this.updateIntent({
8074
+ gateway: 'stripe',
8075
+ intent: { id, payment_method: token },
8076
+ });
7776
8077
 
7777
- return result;
8078
+ if (intent.error) {
8079
+ throw new Error(intent.error.message);
8080
+ }
8081
+
8082
+ await this.loadScripts(this.scripts);
8083
+
8084
+ return await this._confirmCardPayment(intent);
7778
8085
  } finally {
7779
8086
  await this.resetAsyncPayment(payment.id);
7780
8087
  }
7781
8088
  }
7782
8089
 
7783
- async _createIntent(cart, paymentMethod) {
7784
- const { account, currency, capture_total, auth_total } = cart;
7785
- const stripeCustomer = account && account.stripe_customer;
7786
- const stripeCurrency = (currency || 'USD').toLowerCase();
7787
- const amount = stripeAmountByCurrency(currency, capture_total + auth_total);
8090
+ async _createIntent(cart, token) {
8091
+ const stripeCustomer = cart.account && cart.account.stripe_customer;
7788
8092
  const intent = await this.createIntent({
7789
8093
  gateway: 'stripe',
7790
8094
  intent: {
7791
- amount,
7792
- currency: stripeCurrency,
7793
- payment_method: paymentMethod.token,
8095
+ amount: intentAmount(cart),
8096
+ currency: intentCurrency(cart),
8097
+ payment_method: token,
7794
8098
  capture_method: 'manual',
7795
8099
  setup_future_usage: 'off_session',
7796
8100
  ...(stripeCustomer ? { customer: stripeCustomer } : {}),
@@ -7801,20 +8105,6 @@ class StripeCardPayment extends Payment {
7801
8105
  throw new Error('Stripe payment intent is not defined');
7802
8106
  }
7803
8107
 
7804
- switch (intent.status) {
7805
- case 'requires_capture':
7806
- case 'requires_confirmation':
7807
- break;
7808
-
7809
- default:
7810
- throw new Error(`Unsupported intent status: ${intent.status}`);
7811
- }
7812
-
7813
- // Confirm the payment intent
7814
- if (intent.status === 'requires_confirmation') {
7815
- await this._confirmCardPayment(intent);
7816
- }
7817
-
7818
8108
  return intent;
7819
8109
  }
7820
8110
 
@@ -7882,24 +8172,58 @@ class StripeIDealPayment extends Payment {
7882
8172
  StripeIDealPayment.stripeElement = stripeElement;
7883
8173
  }
7884
8174
 
7885
- async createElements() {
8175
+ /** @returns {import('@stripe/stripe-js').StripeElements} */
8176
+ get stripeElements() {
8177
+ return StripeIDealPayment.stripeElements;
8178
+ }
8179
+
8180
+ /** @param {import('@stripe/stripe-js').StripeElements} stripeElements */
8181
+ set stripeElements(stripeElements) {
8182
+ StripeIDealPayment.stripeElements = stripeElements;
8183
+ }
8184
+
8185
+ /** @param {import('../../../types').Cart} cart */
8186
+ async createElements(cart) {
7886
8187
  await this.loadScripts(this.scripts);
7887
8188
 
8189
+ if (this.params.paymentElement) {
8190
+ this.stripeElements = createPaymentElements(
8191
+ this.stripe,
8192
+ getIDealElementsOptions(cart),
8193
+ this.params,
8194
+ );
8195
+
8196
+ return;
8197
+ }
8198
+
7888
8199
  const elements = this.stripe.elements(this.params.config);
7889
8200
 
7890
8201
  this.stripeElement = createElement('idealBank', elements, this.params);
7891
8202
  }
7892
8203
 
7893
8204
  async tokenize() {
7894
- if (!this.stripeElement) {
8205
+ const { paymentElement } = this.params;
8206
+
8207
+ if (paymentElement && !this.stripeElements) {
7895
8208
  throw new Error('Stripe payment element is not defined');
7896
8209
  }
7897
8210
 
7898
8211
  await this.loadScripts(this.scripts);
7899
8212
 
7900
8213
  const cart = await this.getCart();
7901
- const { paymentMethod, error: paymentMethodError } =
7902
- await createIDealPaymentMethod(this.stripe, this.stripeElement, cart);
8214
+ const { paymentMethod, error: paymentMethodError } = paymentElement
8215
+ ? await createPaymentMethodFromElements(
8216
+ this.stripe,
8217
+ this.stripeElements,
8218
+ getIDealElementsOptions(cart),
8219
+ cart,
8220
+ )
8221
+ : // without an idealBank element the shopper picks the bank on the iDEAL page
8222
+ await createIDealPaymentMethod(
8223
+ this.stripe,
8224
+ this.stripeElement || {},
8225
+ cart,
8226
+ );
7903
8227
 
7904
8228
  if (paymentMethodError) {
7905
8229
  throw new Error(paymentMethodError.message);
@@ -7910,6 +8234,38 @@ class StripeIDealPayment extends Payment {
7910
8234
  await this.stripe.handleCardAction(intent.client_secret);
7911
8235
  }
7912
8236
 
8237
+ async handleRedirect(queryParams) {
8238
+ const { redirect_status, payment_intent_client_secret } = queryParams;
8239
+
8240
+ if (redirect_status !== 'succeeded') {
8241
+ throw new UnableAuthenticatePaymentMethodError();
8242
+ }
8243
+
8244
+ await this.loadScripts(this.scripts);
8245
+
8246
+ const { paymentIntent, error } = await this.stripe.retrievePaymentIntent(
8247
+ payment_intent_client_secret,
8248
+ );
8249
+
8250
+ if (error) {
8251
+ throw new Error(error.message);
8252
+ }
8253
+
8254
+ if (
8255
+ !['succeeded', 'requires_capture', 'processing'].includes(
8256
+ paymentIntent.status,
8257
+ )
8258
+ ) {
8259
+ throw new Error(
8260
+ paymentIntent.last_payment_error?.message ||
8261
+ `Unsupported intent status (${paymentIntent.status})`,
8262
+ );
8263
+ }
8264
+
8265
+ // the cart's billing was written in `tokenize`
8266
+ this.onSuccess();
8267
+ }
8268
+
7913
8269
  /**
7914
8270
  * @param {object} cart
7915
8271
  * @param {import('@stripe/stripe-js').PaymentMethod} paymentMethod
@@ -7927,7 +8283,8 @@ class StripeIDealPayment extends Payment {
7927
8283
  payment_method_types: 'ideal',
7928
8284
  confirmation_method: 'manual',
7929
8285
  confirm: true,
7930
- return_url: window.location.href,
8286
+ // `gateway` lets `handleRedirect` run on return; Stripe appends the intent and `redirect_status`
8287
+ return_url: getReturnUrl(),
7931
8288
  },
7932
8289
  });
7933
8290
 
@@ -7959,6 +8316,14 @@ class StripeIDealPayment extends Payment {
7959
8316
  }
7960
8317
  }
7961
8318
 
8319
+ function getReturnUrl() {
8320
+ const url = new URL(window.location.href);
8321
+
8322
+ url.searchParams.set('gateway', 'stripe');
8323
+
8324
+ return url.toString();
8325
+ }
8326
+
7962
8327
  /** @typedef {import('@stripe/stripe-js').Stripe} Stripe */
7963
8328
 
7964
8329
  class StripeBancontactPayment extends Payment {
@@ -8278,6 +8643,7 @@ class StripeGooglePayment extends Payment {
8278
8643
  async _onPaymentMethod(event) {
8279
8644
  const {
8280
8645
  payerEmail,
8646
+ payerName,
8281
8647
  paymentMethod: { id: paymentMethod, card, billing_details },
8282
8648
  shippingAddress,
8283
8649
  shippingOption,
@@ -8296,6 +8662,8 @@ class StripeGooglePayment extends Payment {
8296
8662
  }),
8297
8663
  billing: {
8298
8664
  ...this._mapBillingAddress(billing_details),
8665
+ // a wallet may leave the card's address without a name; the payer's, when asked for, fills it
8666
+ name: billing_details?.name || payerName,
8299
8667
  method: 'card',
8300
8668
  card: {
8301
8669
  gateway: 'stripe',
@@ -8534,6 +8902,7 @@ class StripeApplePayment extends Payment {
8534
8902
  async _onPaymentMethod(event) {
8535
8903
  const {
8536
8904
  payerEmail,
8905
+ payerName,
8537
8906
  paymentMethod: { id: paymentMethod, card, billing_details },
8538
8907
  shippingAddress,
8539
8908
  shippingOption,
@@ -8552,6 +8921,8 @@ class StripeApplePayment extends Payment {
8552
8921
  }),
8553
8922
  billing: {
8554
8923
  ...this._mapBillingAddress(billing_details),
8924
+ // a wallet may leave the card's address without a name; the payer's, when asked for, fills it
8925
+ name: billing_details?.name || payerName,
8555
8926
  method: 'card',
8556
8927
  card: {
8557
8928
  gateway: 'stripe',
@@ -10982,6 +11353,238 @@ class QuickpayCardPayment extends Payment {
10982
11353
  }
10983
11354
  }
10984
11355
 
11356
+ // Saferpay payment-page brands per Swell method id; a list is joined because the
11357
+ // vault rejects arrays in `PaymentMethods`
11358
+ const PAYMENT_METHODS = {
11359
+ card: [
11360
+ 'AMEX',
11361
+ 'DINERS',
11362
+ 'MAESTRO',
11363
+ 'JCB',
11364
+ 'VISA',
11365
+ 'BANCONTACT',
11366
+ 'MASTERCARD',
11367
+ 'SAFERPAYTEST',
11368
+ 'UNIONPAY',
11369
+ ].join(','),
11370
+ twint: 'TWINT',
11371
+ };
11372
+
11373
+ /**
11374
+ * @param {string} method 'card' or 'twint'
11375
+ * @returns {string}
11376
+ */
11377
+ function getSaferpayPaymentMethods(method) {
11378
+ return PAYMENT_METHODS[method];
11379
+ }
11380
+
11381
+ /**
11382
+ * The Saferpay payment page request for a cart.
11383
+ *
11384
+ * @param {object} cart
11385
+ * @param {string} method 'card' or 'twint'
11386
+ * @returns {object}
11387
+ */
11388
+ function getSaferpayPaymentPageData(cart, method) {
11389
+ const currency = (cart.currency || 'USD').toUpperCase();
11390
+ const returnUrl = window.location.origin + window.location.pathname;
11391
+
11392
+ return {
11393
+ Payment: {
11394
+ Amount: {
11395
+ Value: amountInCents(currency, cart.grand_total || 0),
11396
+ CurrencyCode: currency,
11397
+ },
11398
+ Description: 'Saferpay payment powered by Swell',
11399
+ },
11400
+ PaymentMethods: getSaferpayPaymentMethods(method),
11401
+ ReturnUrls: {
11402
+ Success: `${returnUrl}?gateway=saferpay&redirect_status=succeeded`,
11403
+ Fail: `${returnUrl}?gateway=saferpay&redirect_status=failed`,
11404
+ Abort: `${returnUrl}?gateway=saferpay&redirect_status=canceled`,
11405
+ },
11406
+ };
11407
+ }
11408
+
11409
+ /**
11410
+ * @param {object} state the Saferpay transaction state
11411
+ * @returns {boolean}
11412
+ */
11413
+ function isSaferpayCardPayment(state) {
11414
+ const brand = state?.PaymentMeans?.Brand;
11415
+
11416
+ return Boolean(
11417
+ state?.RegistrationResult?.Success &&
11418
+ brand &&
11419
+ PAYMENT_METHODS.card.split(',').includes(brand.PaymentMethod),
11420
+ );
11421
+ }
11422
+
11423
+ /**
11424
+ * @param {object} state the Saferpay transaction state
11425
+ * @returns {boolean}
11426
+ */
11427
+ function isSaferpayTwintPayment(state) {
11428
+ return state?.PaymentMeans?.Brand?.PaymentMethod === 'TWINT';
11429
+ }
11430
+
11431
+ /**
11432
+ * The card stored on the cart's billing from a Saferpay card state.
11433
+ *
11434
+ * @param {object} state
11435
+ * @returns {object}
11436
+ */
11437
+ function getSaferpayCard(state) {
11438
+ const {
11439
+ RegistrationResult: {
11440
+ Alias: { Id: token },
11441
+ },
11442
+ PaymentMeans: {
11443
+ Card: { ExpMonth: exp_month, ExpYear: exp_year, MaskedNumber },
11444
+ Brand: { Name: brand },
11445
+ },
11446
+ } = state;
11447
+
11448
+ return {
11449
+ token,
11450
+ exp_month,
11451
+ exp_year,
11452
+ brand,
11453
+ last4: MaskedNumber.slice(-4),
11454
+ gateway: 'saferpay',
11455
+ };
11456
+ }
11457
+
11458
+ /**
11459
+ * @param {object} state
11460
+ * @returns {string | undefined}
11461
+ */
11462
+ function getSaferpayTransactionId(state) {
11463
+ return state?.Transaction?.Id;
11464
+ }
11465
+
11466
+ /**
11467
+ * Saferpay payment page: `tokenize` sends the shopper to Saferpay, and
11468
+ * `handleRedirect` turns the returned transaction into the cart's billing.
11469
+ * The card and TWINT classes share it; each one handles only the redirect of
11470
+ * the method the cart was tokenized with, so a host can pass both.
11471
+ */
11472
+ class SaferpayPayment extends Payment {
11473
+ constructor(api, options, params, methods, methodId) {
11474
+ super(api, options, params, methods[methodId]);
11475
+ this.methodId = methodId;
11476
+ this.cardMethod = methods.card;
11477
+ }
11478
+
11479
+ async tokenize() {
11480
+ const cart = await this.getCart();
11481
+
11482
+ // a card Saferpay already registered on this cart pays again without a new page visit
11483
+ if (
11484
+ this.methodId === 'card' &&
11485
+ cart.billing?.card?.gateway === 'saferpay' &&
11486
+ !this.params?.replace
11487
+ ) {
11488
+ return this.onSuccess();
11489
+ }
11490
+
11491
+ const intent = await this.createIntent({
11492
+ gateway: 'saferpay',
11493
+ intent: getSaferpayPaymentPageData(cart, this.methodId),
11494
+ });
11495
+
11496
+ if (!intent) {
11497
+ throw new Error('Saferpay payment page is not defined');
11498
+ }
11499
+
11500
+ await this.updateCart({
11501
+ billing: {
11502
+ method: this.methodId,
11503
+ intent: {
11504
+ saferpay: {
11505
+ token: intent.token,
11506
+ },
11507
+ },
11508
+ },
11509
+ });
11510
+
11511
+ window.location.replace(intent.redirect_url);
11512
+ }
11513
+
11514
+ async handleRedirect(queryParams) {
11515
+ const { redirect_status: status } = queryParams;
11516
+ const cart = await this.getCart();
11517
+
11518
+ // another Saferpay method of this host was tokenized: its instance answers
11519
+ if (cart.billing?.method !== this.methodId) {
11520
+ return;
11521
+ }
11522
+
11523
+ switch (status) {
11524
+ case 'succeeded':
11525
+ return this._handleSuccessfulRedirect(cart);
11526
+ case 'canceled':
11527
+ return this.onCancel();
11528
+ case 'failed':
11529
+ throw new UnableAuthenticatePaymentMethodError();
11530
+ default:
11531
+ throw new Error(`Unknown redirect status: ${status}.`);
11532
+ }
11533
+ }
11534
+
11535
+ async _handleSuccessfulRedirect(cart) {
11536
+ const token = cart.billing?.intent?.saferpay?.token;
11537
+
11538
+ if (!token) {
11539
+ throw new Error('Saferpay transaction token is not defined');
11540
+ }
11541
+
11542
+ const intent = await this.updateIntent({
11543
+ gateway: 'saferpay',
11544
+ intent: { token },
11545
+ });
11546
+
11547
+ if (!intent) {
11548
+ throw new Error('Saferpay transaction is not defined');
11549
+ }
11550
+
11551
+ const { state } = intent;
11552
+ const transaction = { transaction_id: getSaferpayTransactionId(state) };
11553
+
11554
+ if (isSaferpayCardPayment(state)) {
11555
+ const card = getSaferpayCard(state);
11556
+
11557
+ if (!isLiveMode(this.cardMethod?.mode)) {
11558
+ card.test = true;
11559
+ }
11560
+
11561
+ await this.updateCart({
11562
+ billing: { method: 'card', card, intent: { saferpay: transaction } },
11563
+ });
11564
+ } else if (isSaferpayTwintPayment(state)) {
11565
+ await this.updateCart({
11566
+ billing: { method: 'twint', intent: { saferpay: transaction } },
11567
+ });
11568
+ } else {
11569
+ throw new Error('Payment method not supported by this gateway.');
11570
+ }
11571
+
11572
+ this.onSuccess();
11573
+ }
11574
+ }
11575
+
11576
+ class SaferpayCardPayment extends SaferpayPayment {
11577
+ constructor(api, options, params, methods) {
11578
+ super(api, options, params, methods, 'card');
11579
+ }
11580
+ }
11581
+
11582
+ class SaferpayTwintPayment extends SaferpayPayment {
11583
+ constructor(api, options, params, methods) {
11584
+ super(api, options, params, methods, 'twint');
11585
+ }
11586
+ }
11587
+
10985
11588
  let convesioSettings = null;
10986
11589
 
10987
11590
  function getBaseConvesioApiUrl(mode) {
@@ -12662,6 +13265,33 @@ class PaymentController {
12662
13265
  return this.api.request('get', '/payments', id);
12663
13266
  }
12664
13267
 
13268
+ /**
13269
+ * Sends the shopper to the gateway when a submitted order waits for an
13270
+ * asynchronous payment; resolves `true` after starting the redirect.
13271
+ *
13272
+ * @param {object} order the result of `cart.submitOrder()`
13273
+ * @returns {Promise<boolean>}
13274
+ */
13275
+ async handleAsyncRedirect(order) {
13276
+ const paymentId =
13277
+ order?.authorized_payment_id ?? order?.authorizedPaymentId;
13278
+
13279
+ if (order?.status !== 'payment_pending' || !paymentId) {
13280
+ return false;
13281
+ }
13282
+
13283
+ const payment = await this.get(paymentId);
13284
+ const url = payment?.async_data?.url ?? payment?.asyncData?.url;
13285
+
13286
+ if (!payment || payment.captured || !url) {
13287
+ return false;
13288
+ }
13289
+
13290
+ window.location.replace(url);
13291
+
13292
+ return true;
13293
+ }
13294
+
12665
13295
  async methods() {
12666
13296
  if (this.methodSettings) {
12667
13297
  return this.methodSettings;
@@ -12679,7 +13309,7 @@ class PaymentController {
12679
13309
  throw new Error('Payment element parameters are not provided');
12680
13310
  }
12681
13311
 
12682
- const paymentInstances = await this._createPaymentInstances();
13312
+ const paymentInstances = await this._createPaymentInstances(params);
12683
13313
  const cart = await this.payment.getCart();
12684
13314
 
12685
13315
  await this._performPaymentAction(
@@ -12698,7 +13328,7 @@ class PaymentController {
12698
13328
  throw new Error('Tokenization parameters are not provided');
12699
13329
  }
12700
13330
 
12701
- const paymentInstances = await this._createPaymentInstances();
13331
+ const paymentInstances = await this._createPaymentInstances(params);
12702
13332
 
12703
13333
  await this._performPaymentAction(paymentInstances, 'tokenize');
12704
13334
  }
@@ -12718,7 +13348,7 @@ class PaymentController {
12718
13348
 
12719
13349
  removeUrlParams();
12720
13350
 
12721
- const paymentInstances = await this._createPaymentInstances();
13351
+ const paymentInstances = await this._createPaymentInstances(params);
12722
13352
 
12723
13353
  await this._performPaymentAction(
12724
13354
  paymentInstances,
@@ -12811,9 +13441,10 @@ class PaymentController {
12811
13441
  /**
12812
13442
  * @returns {Promise<Payment[]>}
12813
13443
  */
12814
- async _createPaymentInstances() {
13444
+ // Takes the caller's params: this.params can change while the methods load (two createElements at once)
13445
+ async _createPaymentInstances(callParams) {
12815
13446
  const paymentMethods = await this._getPaymentMethods();
12816
- const params = adjustParams(this.params);
13447
+ const params = adjustParams(callParams);
12817
13448
 
12818
13449
  return Object.entries(params).reduce((acc, [method, params]) => {
12819
13450
  const methodSettings = paymentMethods[method];
@@ -12911,6 +13542,8 @@ class PaymentController {
12911
13542
  return this._getAmazonPaymentClass(gateway);
12912
13543
  case 'sezzle':
12913
13544
  return this._getSezzlePaymentClass(gateway);
13545
+ case 'twint':
13546
+ return this._getTwintPaymentClass(gateway);
12914
13547
  default:
12915
13548
  return null;
12916
13549
  }
@@ -12924,6 +13557,17 @@ class PaymentController {
12924
13557
  return QuickpayCardPayment;
12925
13558
  case 'convesiopay':
12926
13559
  return ConvesioCardPayment;
13560
+ case 'saferpay':
13561
+ return SaferpayCardPayment;
13562
+ default:
13563
+ return null;
13564
+ }
13565
+ }
13566
+
13567
+ _getTwintPaymentClass(gateway) {
13568
+ switch (gateway) {
13569
+ case 'saferpay':
13570
+ return SaferpayTwintPayment;
12927
13571
  default:
12928
13572
  return null;
12929
13573
  }
@@ -13475,7 +14119,7 @@ function swell(initStore = undefined, initKey, initOptions = {}) {
13475
14119
  const api = {};
13476
14120
 
13477
14121
  Object.assign(api, {
13478
- version: '5.9.4',
14122
+ version: '5.10.0',
13479
14123
  options,
13480
14124
  request,
13481
14125