@funnelfox/billing 0.9.0-beta.7 → 0.9.0-ffb-467-adyen.13

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.
@@ -491,7 +491,7 @@ exports.PaymentMethod = void 0;
491
491
  /**
492
492
  * @fileoverview Constants for Funnefox SDK
493
493
  */
494
- const SDK_VERSION = '0.9.0-beta.7';
494
+ const SDK_VERSION = '0.9.0-beta.1';
495
495
  const DEFAULTS = {
496
496
  BASE_URL: 'https://billing.funnelfox.com',
497
497
  REGION: 'default',
@@ -530,9 +530,9 @@ const API_ENDPOINTS = {
530
530
  UPDATE_CLIENT_SESSION: '/v1/checkout/update_client_session',
531
531
  CREATE_PAYMENT: '/v1/checkout/create_payment',
532
532
  RESUME_PAYMENT: '/v1/checkout/resume_payment',
533
- STRIPE_CREATE_PAYMENT: '/v1/stripe/create_payment',
534
533
  ONE_CLICK: '/v1/checkout/one_click',
535
534
  CREATE_SIMPLE_CLIENT_SESSION: '/v1/checkout/create_simple_client_session',
535
+ RECALCULATE_TAX: '/v1/checkout/recalculate_tax',
536
536
  };
537
537
  const ERROR_CODES = {
538
538
  SDK_ERROR: 'SDK_ERROR',
@@ -1170,19 +1170,34 @@ class APIClient {
1170
1170
  if (params.postalCode !== undefined) {
1171
1171
  payload.postal_code = params.postalCode;
1172
1172
  }
1173
+ if (params.subdivision !== undefined) {
1174
+ payload.subdivision = params.subdivision;
1175
+ }
1176
+ if (params.taxCalculationId !== undefined) {
1177
+ payload.tax_calculation_id = params.taxCalculationId;
1178
+ }
1173
1179
  return (await this.request(API_ENDPOINTS.CREATE_PAYMENT, {
1174
1180
  method: 'POST',
1175
1181
  body: JSON.stringify(payload),
1176
1182
  }));
1177
1183
  }
1178
- async createStripePayment(params) {
1179
- return (await this.request(API_ENDPOINTS.STRIPE_CREATE_PAYMENT, {
1184
+ async recalculateTax(params) {
1185
+ const payload = {
1186
+ order_id: params.orderId,
1187
+ client_token: params.clientToken,
1188
+ country_code: params.countryCode,
1189
+ };
1190
+ if (params.postalCode) {
1191
+ payload.postal_code = params.postalCode;
1192
+ }
1193
+ if (params.subdivision) {
1194
+ payload.subdivision = params.subdivision;
1195
+ }
1196
+ const raw = (await this.request(API_ENDPOINTS.RECALCULATE_TAX, {
1180
1197
  method: 'POST',
1181
- body: JSON.stringify({
1182
- order_id: params.orderId,
1183
- payment_method_id: params.paymentMethodId,
1184
- }),
1198
+ body: JSON.stringify(payload),
1185
1199
  }));
1200
+ return raw.data;
1186
1201
  }
1187
1202
  async resumePayment(params) {
1188
1203
  const payload = {
@@ -1763,6 +1778,7 @@ function getPageUrl() {
1763
1778
  /**
1764
1779
  * @fileoverview Checkout instance manager for Funnefox SDK
1765
1780
  */
1781
+ const TAX_RECALC_DEBOUNCE_MS = 600;
1766
1782
  class CheckoutInstance extends EventEmitter {
1767
1783
  constructor(config) {
1768
1784
  super();
@@ -1798,11 +1814,71 @@ class CheckoutInstance extends EventEmitter {
1798
1814
  if (!this.isPostalCodeVisible()) {
1799
1815
  this.cardPostalCode = undefined;
1800
1816
  }
1801
- return;
1802
1817
  }
1803
- if (inputName === 'postalCode') {
1818
+ else if (inputName === 'postalCode') {
1804
1819
  this.cardPostalCode = value?.trim() || undefined;
1805
1820
  }
1821
+ if (this.isTaxEnabled()) {
1822
+ this.scheduleTaxRecalc();
1823
+ }
1824
+ };
1825
+ // Tax is driven by the tenant's tax_calculation_provider setting (surfaced as session.tax_enabled).
1826
+ // enableTax stays as a legacy host override only when the backend doesn't report the flag.
1827
+ this.isTaxEnabled = () => this.cachedSessionResponse?.data?.tax_enabled ??
1828
+ this.checkoutConfig.enableTax ??
1829
+ false;
1830
+ this.emitSessionTaxEstimate = () => {
1831
+ const data = this.cachedSessionResponse?.data;
1832
+ if (!data || data.amount_total == null) {
1833
+ return;
1834
+ }
1835
+ this.checkoutConfig.onTaxChange?.({
1836
+ amountTotal: data.amount_total,
1837
+ taxAmount: data.tax_amount ?? 0,
1838
+ currency: data.currency ?? '',
1839
+ });
1840
+ };
1841
+ this.runTaxRecalc = async () => {
1842
+ const orderId = this.orderId;
1843
+ const countryCode = this.getSelectedCountryCode();
1844
+ if (!orderId || !countryCode) {
1845
+ return;
1846
+ }
1847
+ try {
1848
+ const tax = await this.apiClient.recalculateTax({
1849
+ orderId,
1850
+ clientToken: this.cachedSessionResponse?.data?.client_token ?? '',
1851
+ countryCode,
1852
+ postalCode: this.cardPostalCode,
1853
+ });
1854
+ this.checkoutConfig.onTaxChange?.({
1855
+ amountTotal: tax.amount_total,
1856
+ taxAmount: tax.tax_amount,
1857
+ currency: tax.currency,
1858
+ });
1859
+ }
1860
+ catch (err) {
1861
+ // Recalc failed for the new address; surface it instead of silently showing a stale total.
1862
+ console.warn('[funnelfox-billing] tax recalculation failed', err);
1863
+ this.checkoutConfig.onTaxError?.(err);
1864
+ }
1865
+ };
1866
+ this.scheduleTaxRecalc = () => {
1867
+ if (this.taxRecalcDebounce) {
1868
+ clearTimeout(this.taxRecalcDebounce);
1869
+ }
1870
+ this.taxRecalcDebounce = setTimeout(this.runTaxRecalc, TAX_RECALC_DEBOUNCE_MS);
1871
+ };
1872
+ // Flush a pending recalc so a card charge reflects the final entered address (no estimate drift).
1873
+ // Used for card payments only; wallets authorize their own amount and are left untouched.
1874
+ this.flushTaxRecalc = async () => {
1875
+ if (this.taxRecalcDebounce) {
1876
+ clearTimeout(this.taxRecalcDebounce);
1877
+ this.taxRecalcDebounce = undefined;
1878
+ }
1879
+ if (this.isTaxEnabled()) {
1880
+ await this.runTaxRecalc();
1881
+ }
1806
1882
  };
1807
1883
  this.handleMethodRender = (method) => {
1808
1884
  this.emit(EVENTS.METHOD_RENDER, method);
@@ -1822,6 +1898,11 @@ class CheckoutInstance extends EventEmitter {
1822
1898
  try {
1823
1899
  this.onLoaderChangeWithRace(true);
1824
1900
  this._setState('processing');
1901
+ // Card charge must reflect the final entered address; wallets authorize their own amount, so
1902
+ // only flush the pending recalc for card payments.
1903
+ if (this.activePaymentMethodType === exports.PaymentMethod.PAYMENT_CARD) {
1904
+ await this.flushTaxRecalc();
1905
+ }
1825
1906
  const [radarSessionId, airwallexDeviceId] = await Promise.all([
1826
1907
  this.cachedSessionResponse?.radarSessionId,
1827
1908
  this.cachedSessionResponse?.airwallexDeviceId,
@@ -1939,6 +2020,9 @@ class CheckoutInstance extends EventEmitter {
1939
2020
  await this.createSession();
1940
2021
  await this._initializePrimerCheckout();
1941
2022
  this._setState('ready');
2023
+ if (this.isTaxEnabled()) {
2024
+ this.emitSessionTaxEstimate();
2025
+ }
1942
2026
  this.startUnhandledTelemetry();
1943
2027
  this.checkoutConfig?.onInitialized?.();
1944
2028
  return this;
@@ -2213,6 +2297,7 @@ class CheckoutInstance extends EventEmitter {
2213
2297
  wasPaymentProcessedStarted = true;
2214
2298
  },
2215
2299
  onTokenizeShouldStart: data => {
2300
+ this.activePaymentMethodType = data.paymentMethodType;
2216
2301
  this.emit(EVENTS.ERROR, undefined);
2217
2302
  this.emit(EVENTS.START_PURCHASE, data.paymentMethodType);
2218
2303
  return true;
@@ -2252,6 +2337,9 @@ class CheckoutInstance extends EventEmitter {
2252
2337
  await this.primerWrapper.refreshClientSession();
2253
2338
  this.onLoaderChangeWithRace(false);
2254
2339
  this._setState('ready');
2340
+ if (this.isTaxEnabled()) {
2341
+ this.scheduleTaxRecalc();
2342
+ }
2255
2343
  }
2256
2344
  catch (error) {
2257
2345
  this.onLoaderChangeWithRace(false);
@@ -2697,6 +2785,28 @@ async function createStripeCardForm(element, params) {
2697
2785
  });
2698
2786
  return mountStripeCardForm(element, session, { ...params, apiClient });
2699
2787
  }
2788
+ async function createAdyenCardForm(element, params) {
2789
+ const config = resolveConfig(params, 'createAdyenCardForm');
2790
+ const [session, { mountAdyenCardForm }] = await Promise.all([
2791
+ sessionService.createSession({
2792
+ orgId: config.orgId,
2793
+ baseUrl: config.baseUrl,
2794
+ region: config.region,
2795
+ priceId: params.priceId,
2796
+ externalId: params.externalId,
2797
+ email: params.email,
2798
+ clientMetadata: params.clientMetadata,
2799
+ countryCode: params.countryCode,
2800
+ integration: 'adyen',
2801
+ }),
2802
+ Promise.resolve().then(function () { return require('./chunk-adyen-card-form.cjs.js'); }),
2803
+ ]);
2804
+ const apiClient = new APIClient({
2805
+ orgId: config.orgId,
2806
+ baseUrl: config.baseUrl || DEFAULTS.BASE_URL,
2807
+ });
2808
+ return mountAdyenCardForm(element, session, { ...params, apiClient });
2809
+ }
2700
2810
  async function purchaseStripeWallet(params) {
2701
2811
  const config = resolveConfig(params, 'purchaseStripeWallet');
2702
2812
  const [session, { purchaseWallet }] = await Promise.all([
@@ -2765,6 +2875,9 @@ const Billing = {
2765
2875
  getAvailableWallet: getAvailableStripeWallet,
2766
2876
  getAvailablePaymentMethods: getAvailableStripePaymentMethods,
2767
2877
  },
2878
+ adyen: {
2879
+ createCardForm: createAdyenCardForm,
2880
+ },
2768
2881
  };
2769
2882
  if (typeof window !== 'undefined') {
2770
2883
  window.Billing = Billing;
@@ -2788,4 +2901,6 @@ exports.configure = configure;
2788
2901
  exports.createCheckout = createCheckout;
2789
2902
  exports.createClientSession = createClientSession;
2790
2903
  exports.getAvailablePaymentMethods = getAvailablePaymentMethods;
2904
+ exports.loadScript = loadScript$1;
2791
2905
  exports.loadStripe = loadStripe;
2906
+ exports.loadStylesheet = loadStylesheet;
@@ -489,7 +489,7 @@ var PaymentMethod;
489
489
  /**
490
490
  * @fileoverview Constants for Funnefox SDK
491
491
  */
492
- const SDK_VERSION = '0.9.0-beta.7';
492
+ const SDK_VERSION = '0.9.0-beta.1';
493
493
  const DEFAULTS = {
494
494
  BASE_URL: 'https://billing.funnelfox.com',
495
495
  REGION: 'default',
@@ -528,9 +528,9 @@ const API_ENDPOINTS = {
528
528
  UPDATE_CLIENT_SESSION: '/v1/checkout/update_client_session',
529
529
  CREATE_PAYMENT: '/v1/checkout/create_payment',
530
530
  RESUME_PAYMENT: '/v1/checkout/resume_payment',
531
- STRIPE_CREATE_PAYMENT: '/v1/stripe/create_payment',
532
531
  ONE_CLICK: '/v1/checkout/one_click',
533
532
  CREATE_SIMPLE_CLIENT_SESSION: '/v1/checkout/create_simple_client_session',
533
+ RECALCULATE_TAX: '/v1/checkout/recalculate_tax',
534
534
  };
535
535
  const ERROR_CODES = {
536
536
  SDK_ERROR: 'SDK_ERROR',
@@ -1168,19 +1168,34 @@ class APIClient {
1168
1168
  if (params.postalCode !== undefined) {
1169
1169
  payload.postal_code = params.postalCode;
1170
1170
  }
1171
+ if (params.subdivision !== undefined) {
1172
+ payload.subdivision = params.subdivision;
1173
+ }
1174
+ if (params.taxCalculationId !== undefined) {
1175
+ payload.tax_calculation_id = params.taxCalculationId;
1176
+ }
1171
1177
  return (await this.request(API_ENDPOINTS.CREATE_PAYMENT, {
1172
1178
  method: 'POST',
1173
1179
  body: JSON.stringify(payload),
1174
1180
  }));
1175
1181
  }
1176
- async createStripePayment(params) {
1177
- return (await this.request(API_ENDPOINTS.STRIPE_CREATE_PAYMENT, {
1182
+ async recalculateTax(params) {
1183
+ const payload = {
1184
+ order_id: params.orderId,
1185
+ client_token: params.clientToken,
1186
+ country_code: params.countryCode,
1187
+ };
1188
+ if (params.postalCode) {
1189
+ payload.postal_code = params.postalCode;
1190
+ }
1191
+ if (params.subdivision) {
1192
+ payload.subdivision = params.subdivision;
1193
+ }
1194
+ const raw = (await this.request(API_ENDPOINTS.RECALCULATE_TAX, {
1178
1195
  method: 'POST',
1179
- body: JSON.stringify({
1180
- order_id: params.orderId,
1181
- payment_method_id: params.paymentMethodId,
1182
- }),
1196
+ body: JSON.stringify(payload),
1183
1197
  }));
1198
+ return raw.data;
1184
1199
  }
1185
1200
  async resumePayment(params) {
1186
1201
  const payload = {
@@ -1761,6 +1776,7 @@ function getPageUrl() {
1761
1776
  /**
1762
1777
  * @fileoverview Checkout instance manager for Funnefox SDK
1763
1778
  */
1779
+ const TAX_RECALC_DEBOUNCE_MS = 600;
1764
1780
  class CheckoutInstance extends EventEmitter {
1765
1781
  constructor(config) {
1766
1782
  super();
@@ -1796,11 +1812,71 @@ class CheckoutInstance extends EventEmitter {
1796
1812
  if (!this.isPostalCodeVisible()) {
1797
1813
  this.cardPostalCode = undefined;
1798
1814
  }
1799
- return;
1800
1815
  }
1801
- if (inputName === 'postalCode') {
1816
+ else if (inputName === 'postalCode') {
1802
1817
  this.cardPostalCode = value?.trim() || undefined;
1803
1818
  }
1819
+ if (this.isTaxEnabled()) {
1820
+ this.scheduleTaxRecalc();
1821
+ }
1822
+ };
1823
+ // Tax is driven by the tenant's tax_calculation_provider setting (surfaced as session.tax_enabled).
1824
+ // enableTax stays as a legacy host override only when the backend doesn't report the flag.
1825
+ this.isTaxEnabled = () => this.cachedSessionResponse?.data?.tax_enabled ??
1826
+ this.checkoutConfig.enableTax ??
1827
+ false;
1828
+ this.emitSessionTaxEstimate = () => {
1829
+ const data = this.cachedSessionResponse?.data;
1830
+ if (!data || data.amount_total == null) {
1831
+ return;
1832
+ }
1833
+ this.checkoutConfig.onTaxChange?.({
1834
+ amountTotal: data.amount_total,
1835
+ taxAmount: data.tax_amount ?? 0,
1836
+ currency: data.currency ?? '',
1837
+ });
1838
+ };
1839
+ this.runTaxRecalc = async () => {
1840
+ const orderId = this.orderId;
1841
+ const countryCode = this.getSelectedCountryCode();
1842
+ if (!orderId || !countryCode) {
1843
+ return;
1844
+ }
1845
+ try {
1846
+ const tax = await this.apiClient.recalculateTax({
1847
+ orderId,
1848
+ clientToken: this.cachedSessionResponse?.data?.client_token ?? '',
1849
+ countryCode,
1850
+ postalCode: this.cardPostalCode,
1851
+ });
1852
+ this.checkoutConfig.onTaxChange?.({
1853
+ amountTotal: tax.amount_total,
1854
+ taxAmount: tax.tax_amount,
1855
+ currency: tax.currency,
1856
+ });
1857
+ }
1858
+ catch (err) {
1859
+ // Recalc failed for the new address; surface it instead of silently showing a stale total.
1860
+ console.warn('[funnelfox-billing] tax recalculation failed', err);
1861
+ this.checkoutConfig.onTaxError?.(err);
1862
+ }
1863
+ };
1864
+ this.scheduleTaxRecalc = () => {
1865
+ if (this.taxRecalcDebounce) {
1866
+ clearTimeout(this.taxRecalcDebounce);
1867
+ }
1868
+ this.taxRecalcDebounce = setTimeout(this.runTaxRecalc, TAX_RECALC_DEBOUNCE_MS);
1869
+ };
1870
+ // Flush a pending recalc so a card charge reflects the final entered address (no estimate drift).
1871
+ // Used for card payments only; wallets authorize their own amount and are left untouched.
1872
+ this.flushTaxRecalc = async () => {
1873
+ if (this.taxRecalcDebounce) {
1874
+ clearTimeout(this.taxRecalcDebounce);
1875
+ this.taxRecalcDebounce = undefined;
1876
+ }
1877
+ if (this.isTaxEnabled()) {
1878
+ await this.runTaxRecalc();
1879
+ }
1804
1880
  };
1805
1881
  this.handleMethodRender = (method) => {
1806
1882
  this.emit(EVENTS.METHOD_RENDER, method);
@@ -1820,6 +1896,11 @@ class CheckoutInstance extends EventEmitter {
1820
1896
  try {
1821
1897
  this.onLoaderChangeWithRace(true);
1822
1898
  this._setState('processing');
1899
+ // Card charge must reflect the final entered address; wallets authorize their own amount, so
1900
+ // only flush the pending recalc for card payments.
1901
+ if (this.activePaymentMethodType === PaymentMethod.PAYMENT_CARD) {
1902
+ await this.flushTaxRecalc();
1903
+ }
1823
1904
  const [radarSessionId, airwallexDeviceId] = await Promise.all([
1824
1905
  this.cachedSessionResponse?.radarSessionId,
1825
1906
  this.cachedSessionResponse?.airwallexDeviceId,
@@ -1937,6 +2018,9 @@ class CheckoutInstance extends EventEmitter {
1937
2018
  await this.createSession();
1938
2019
  await this._initializePrimerCheckout();
1939
2020
  this._setState('ready');
2021
+ if (this.isTaxEnabled()) {
2022
+ this.emitSessionTaxEstimate();
2023
+ }
1940
2024
  this.startUnhandledTelemetry();
1941
2025
  this.checkoutConfig?.onInitialized?.();
1942
2026
  return this;
@@ -2211,6 +2295,7 @@ class CheckoutInstance extends EventEmitter {
2211
2295
  wasPaymentProcessedStarted = true;
2212
2296
  },
2213
2297
  onTokenizeShouldStart: data => {
2298
+ this.activePaymentMethodType = data.paymentMethodType;
2214
2299
  this.emit(EVENTS.ERROR, undefined);
2215
2300
  this.emit(EVENTS.START_PURCHASE, data.paymentMethodType);
2216
2301
  return true;
@@ -2250,6 +2335,9 @@ class CheckoutInstance extends EventEmitter {
2250
2335
  await this.primerWrapper.refreshClientSession();
2251
2336
  this.onLoaderChangeWithRace(false);
2252
2337
  this._setState('ready');
2338
+ if (this.isTaxEnabled()) {
2339
+ this.scheduleTaxRecalc();
2340
+ }
2253
2341
  }
2254
2342
  catch (error) {
2255
2343
  this.onLoaderChangeWithRace(false);
@@ -2695,6 +2783,28 @@ async function createStripeCardForm(element, params) {
2695
2783
  });
2696
2784
  return mountStripeCardForm(element, session, { ...params, apiClient });
2697
2785
  }
2786
+ async function createAdyenCardForm(element, params) {
2787
+ const config = resolveConfig(params, 'createAdyenCardForm');
2788
+ const [session, { mountAdyenCardForm }] = await Promise.all([
2789
+ sessionService.createSession({
2790
+ orgId: config.orgId,
2791
+ baseUrl: config.baseUrl,
2792
+ region: config.region,
2793
+ priceId: params.priceId,
2794
+ externalId: params.externalId,
2795
+ email: params.email,
2796
+ clientMetadata: params.clientMetadata,
2797
+ countryCode: params.countryCode,
2798
+ integration: 'adyen',
2799
+ }),
2800
+ import('./chunk-adyen-card-form.es.js'),
2801
+ ]);
2802
+ const apiClient = new APIClient({
2803
+ orgId: config.orgId,
2804
+ baseUrl: config.baseUrl || DEFAULTS.BASE_URL,
2805
+ });
2806
+ return mountAdyenCardForm(element, session, { ...params, apiClient });
2807
+ }
2698
2808
  async function purchaseStripeWallet(params) {
2699
2809
  const config = resolveConfig(params, 'purchaseStripeWallet');
2700
2810
  const [session, { purchaseWallet }] = await Promise.all([
@@ -2763,9 +2873,12 @@ const Billing = {
2763
2873
  getAvailableWallet: getAvailableStripeWallet,
2764
2874
  getAvailablePaymentMethods: getAvailableStripePaymentMethods,
2765
2875
  },
2876
+ adyen: {
2877
+ createCardForm: createAdyenCardForm,
2878
+ },
2766
2879
  };
2767
2880
  if (typeof window !== 'undefined') {
2768
2881
  window.Billing = Billing;
2769
2882
  }
2770
2883
 
2771
- export { APIError as A, Billing as B, CheckoutError as C, DEFAULT_BUTTONS_OPTIONS as D, EVENTS as E, FunnefoxSDKError as F, NetworkError as N, PaymentMethod as P, SDK_VERSION as S, ValidationError as V, PrimerError as a, ConfigurationError as b, DEFAULTS as c, CHECKOUT_STATES as d, ERROR_CODES as e, configure as f, createCheckout as g, createClientSession as h, getAvailablePaymentMethods as i, loadStripe as l };
2884
+ export { APIError as A, Billing as B, CheckoutError as C, DEFAULT_BUTTONS_OPTIONS as D, EVENTS as E, FunnefoxSDKError as F, NetworkError as N, PaymentMethod as P, SDK_VERSION as S, ValidationError as V, loadScript$1 as a, loadStripe as b, PrimerError as c, ConfigurationError as d, DEFAULTS as e, CHECKOUT_STATES as f, ERROR_CODES as g, configure as h, createCheckout as i, createClientSession as j, getAvailablePaymentMethods as k, loadStylesheet as l };
@@ -10,17 +10,22 @@
10
10
  var stripeLoader = require('./chunk-stripe-loader.cjs.js');
11
11
  require('./chunk-index.cjs.js');
12
12
 
13
+ const TAX_RECALC_DEBOUNCE_MS = 600;
13
14
  async function mountStripeCardForm(element, session, params) {
14
- const { stripe_public_key, amount, currency, order_id } = session.data;
15
+ const { stripe_public_key, amount, currency, order_id, is_link_enabled } = session.data;
16
+ // Tax flow is driven by the tenant setting (session.tax_enabled); enableTax is a legacy override.
17
+ const taxEnabled = session.data.tax_enabled ?? params.enableTax ?? false;
15
18
  const stripe = await stripeLoader.getStripe(stripe_public_key);
16
19
  if (!stripe)
17
20
  throw new Error('Failed to load Stripe');
21
+ // Remounting (e.g. picking another price) must replace the form, not stack a second one.
22
+ element.replaceChildren();
18
23
  const stripeElements = stripe.elements({
19
24
  mode: 'subscription',
20
25
  amount,
21
26
  currency,
22
27
  paymentMethodCreation: 'manual',
23
- paymentMethodTypes: ['card', 'link'],
28
+ paymentMethodTypes: is_link_enabled ? ['card', 'link'] : ['card'],
24
29
  appearance: params.appearance,
25
30
  });
26
31
  const paymentElement = stripeElements.create('payment', {
@@ -31,7 +36,81 @@ async function mountStripeCardForm(element, session, params) {
31
36
  },
32
37
  terms: { card: 'never' },
33
38
  });
34
- paymentElement.mount(element);
39
+ // Tax mode mounts Stripe's Address Element (its change events are readable, so tax recalculates live
40
+ // as the buyer edits the address) above the Payment Element. The country is pre-filled from the
41
+ // detected location so tax is computed from the address (not a separate IP estimate); the resulting
42
+ // calculation id is attached to the PaymentIntent at payment time so Stripe records the tax.
43
+ let taxCalculationId;
44
+ let taxAddress = {};
45
+ // Flushed on submit so the charge always reflects the final entered address (no estimate drift).
46
+ let flushTaxRecalc = async () => { };
47
+ if (taxEnabled) {
48
+ const addressContainer = document.createElement('div');
49
+ const paymentContainer = document.createElement('div');
50
+ element.appendChild(addressContainer);
51
+ element.appendChild(paymentContainer);
52
+ const addressElement = stripeElements.create('address', {
53
+ mode: 'billing',
54
+ fields: { phone: 'never' },
55
+ ...(session.data.detected_country_code
56
+ ? {
57
+ defaultValues: {
58
+ address: { country: session.data.detected_country_code },
59
+ },
60
+ }
61
+ : {}),
62
+ });
63
+ addressElement.mount(addressContainer);
64
+ paymentElement.mount(paymentContainer);
65
+ const runRecalc = async () => {
66
+ const country = taxAddress.country;
67
+ if (!country)
68
+ return;
69
+ try {
70
+ const tax = await params.apiClient.recalculateTax({
71
+ orderId: order_id,
72
+ clientToken: session.data.client_token,
73
+ countryCode: country,
74
+ postalCode: taxAddress.postalCode,
75
+ subdivision: taxAddress.state,
76
+ });
77
+ taxCalculationId = tax.tax_calculation_id;
78
+ stripeElements.update({ amount: tax.amount_total });
79
+ params.onTaxChange?.({
80
+ amountTotal: tax.amount_total,
81
+ taxAmount: tax.tax_amount,
82
+ currency: tax.currency,
83
+ });
84
+ }
85
+ catch {
86
+ // Best-effort: keep the current total if recalculation fails.
87
+ }
88
+ };
89
+ let debounce;
90
+ flushTaxRecalc = async () => {
91
+ if (debounce) {
92
+ clearTimeout(debounce);
93
+ debounce = undefined;
94
+ }
95
+ await runRecalc();
96
+ };
97
+ addressElement.on('change', event => {
98
+ const address = event.value.address;
99
+ taxAddress = {
100
+ country: address.country,
101
+ postalCode: address.postal_code || undefined,
102
+ state: address.state || undefined,
103
+ };
104
+ if (!taxAddress.country)
105
+ return;
106
+ if (debounce)
107
+ clearTimeout(debounce);
108
+ debounce = setTimeout(runRecalc, TAX_RECALC_DEBOUNCE_MS);
109
+ });
110
+ }
111
+ else {
112
+ paymentElement.mount(element);
113
+ }
35
114
  await new Promise((resolve, reject) => {
36
115
  paymentElement.once('ready', () => resolve());
37
116
  // 'loaderror' is a valid Stripe event but not yet in the @stripe/stripe-js types
@@ -42,6 +121,7 @@ async function mountStripeCardForm(element, session, params) {
42
121
  submit: async () => {
43
122
  params.onLoaderChange?.(true);
44
123
  try {
124
+ await flushTaxRecalc();
45
125
  const { error: submitError } = await stripeElements.submit();
46
126
  if (submitError)
47
127
  throw submitError;
@@ -50,19 +130,23 @@ async function mountStripeCardForm(element, session, params) {
50
130
  });
51
131
  if (error)
52
132
  throw error;
53
- const raw = await params.apiClient.createStripePayment({
133
+ const raw = await params.apiClient.createPayment({
54
134
  orderId: order_id,
55
- paymentMethodId: paymentMethod.id,
135
+ paymentMethodToken: paymentMethod.id,
136
+ email: params.email,
137
+ countryCode: taxEnabled ? taxAddress.country : params.countryCode,
138
+ postalCode: taxEnabled ? taxAddress.postalCode : undefined,
139
+ subdivision: taxEnabled ? taxAddress.state : undefined,
140
+ taxCalculationId: taxEnabled ? taxCalculationId : undefined,
141
+ clientMetadata: params.clientMetadata,
56
142
  });
57
143
  const result = params.apiClient.processPaymentResponse(raw);
58
144
  if (result.type === 'action_required') {
59
- const { error: confirmError } = await stripe.confirmPayment({
145
+ const { error: actionError } = await stripe.handleNextAction({
60
146
  clientSecret: result.clientToken,
61
- redirect: 'if_required',
62
- confirmParams: { return_url: window.location.href },
63
147
  });
64
- if (confirmError)
65
- throw confirmError;
148
+ if (actionError)
149
+ throw actionError;
66
150
  }
67
151
  params.onPaymentSuccess?.(paymentMethod, order_id);
68
152
  }