@funnelfox/billing 0.8.0-ffb-395.10 → 0.8.0-ffb-395.14
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/chunk-index.cjs.js +102 -3
- package/dist/chunk-index.es.js +102 -3
- package/dist/chunk-stripe-card-form.cjs.js +85 -2
- package/dist/chunk-stripe-card-form.es.js +85 -2
- package/dist/chunk-stripe-wallet.cjs.js +31 -2
- package/dist/chunk-stripe-wallet.es.js +31 -2
- package/dist/funnelfox-billing.js +218 -7
- package/dist/funnelfox-billing.min.js +1 -1
- package/package.json +6 -2
- package/src/types.d.ts +41 -0
package/dist/chunk-index.cjs.js
CHANGED
|
@@ -491,7 +491,7 @@ exports.PaymentMethod = void 0;
|
|
|
491
491
|
/**
|
|
492
492
|
* @fileoverview Constants for Funnefox SDK
|
|
493
493
|
*/
|
|
494
|
-
const SDK_VERSION = '0.
|
|
494
|
+
const SDK_VERSION = '0.8.0-ffb-395.14';
|
|
495
495
|
const DEFAULTS = {
|
|
496
496
|
BASE_URL: 'https://billing.funnelfox.com',
|
|
497
497
|
REGION: 'default',
|
|
@@ -532,6 +532,7 @@ const API_ENDPOINTS = {
|
|
|
532
532
|
RESUME_PAYMENT: '/v1/checkout/resume_payment',
|
|
533
533
|
ONE_CLICK: '/v1/checkout/one_click',
|
|
534
534
|
CREATE_SIMPLE_CLIENT_SESSION: '/v1/checkout/create_simple_client_session',
|
|
535
|
+
RECALCULATE_TAX: '/v1/checkout/recalculate_tax',
|
|
535
536
|
};
|
|
536
537
|
const ERROR_CODES = {
|
|
537
538
|
SDK_ERROR: 'SDK_ERROR',
|
|
@@ -1169,11 +1170,35 @@ class APIClient {
|
|
|
1169
1170
|
if (params.postalCode !== undefined) {
|
|
1170
1171
|
payload.postal_code = params.postalCode;
|
|
1171
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
|
+
}
|
|
1172
1179
|
return (await this.request(API_ENDPOINTS.CREATE_PAYMENT, {
|
|
1173
1180
|
method: 'POST',
|
|
1174
1181
|
body: JSON.stringify(payload),
|
|
1175
1182
|
}));
|
|
1176
1183
|
}
|
|
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, {
|
|
1197
|
+
method: 'POST',
|
|
1198
|
+
body: JSON.stringify(payload),
|
|
1199
|
+
}));
|
|
1200
|
+
return raw.data;
|
|
1201
|
+
}
|
|
1177
1202
|
async resumePayment(params) {
|
|
1178
1203
|
const payload = {
|
|
1179
1204
|
order_id: params.orderId,
|
|
@@ -1753,6 +1778,7 @@ function getPageUrl() {
|
|
|
1753
1778
|
/**
|
|
1754
1779
|
* @fileoverview Checkout instance manager for Funnefox SDK
|
|
1755
1780
|
*/
|
|
1781
|
+
const TAX_RECALC_DEBOUNCE_MS = 600;
|
|
1756
1782
|
class CheckoutInstance extends EventEmitter {
|
|
1757
1783
|
constructor(config) {
|
|
1758
1784
|
super();
|
|
@@ -1788,11 +1814,71 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1788
1814
|
if (!this.isPostalCodeVisible()) {
|
|
1789
1815
|
this.cardPostalCode = undefined;
|
|
1790
1816
|
}
|
|
1791
|
-
return;
|
|
1792
1817
|
}
|
|
1793
|
-
if (inputName === 'postalCode') {
|
|
1818
|
+
else if (inputName === 'postalCode') {
|
|
1794
1819
|
this.cardPostalCode = value?.trim() || undefined;
|
|
1795
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
|
+
}
|
|
1796
1882
|
};
|
|
1797
1883
|
this.handleMethodRender = (method) => {
|
|
1798
1884
|
this.emit(EVENTS.METHOD_RENDER, method);
|
|
@@ -1812,6 +1898,11 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1812
1898
|
try {
|
|
1813
1899
|
this.onLoaderChangeWithRace(true);
|
|
1814
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
|
+
}
|
|
1815
1906
|
const [radarSessionId, airwallexDeviceId] = await Promise.all([
|
|
1816
1907
|
this.cachedSessionResponse?.radarSessionId,
|
|
1817
1908
|
this.cachedSessionResponse?.airwallexDeviceId,
|
|
@@ -1929,6 +2020,9 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1929
2020
|
await this.createSession();
|
|
1930
2021
|
await this._initializePrimerCheckout();
|
|
1931
2022
|
this._setState('ready');
|
|
2023
|
+
if (this.isTaxEnabled()) {
|
|
2024
|
+
this.emitSessionTaxEstimate();
|
|
2025
|
+
}
|
|
1932
2026
|
this.startUnhandledTelemetry();
|
|
1933
2027
|
this.checkoutConfig?.onInitialized?.();
|
|
1934
2028
|
return this;
|
|
@@ -2203,6 +2297,7 @@ class CheckoutInstance extends EventEmitter {
|
|
|
2203
2297
|
wasPaymentProcessedStarted = true;
|
|
2204
2298
|
},
|
|
2205
2299
|
onTokenizeShouldStart: data => {
|
|
2300
|
+
this.activePaymentMethodType = data.paymentMethodType;
|
|
2206
2301
|
this.emit(EVENTS.ERROR, undefined);
|
|
2207
2302
|
this.emit(EVENTS.START_PURCHASE, data.paymentMethodType);
|
|
2208
2303
|
return true;
|
|
@@ -2242,6 +2337,9 @@ class CheckoutInstance extends EventEmitter {
|
|
|
2242
2337
|
await this.primerWrapper.refreshClientSession();
|
|
2243
2338
|
this.onLoaderChangeWithRace(false);
|
|
2244
2339
|
this._setState('ready');
|
|
2340
|
+
if (this.isTaxEnabled()) {
|
|
2341
|
+
this.scheduleTaxRecalc();
|
|
2342
|
+
}
|
|
2245
2343
|
}
|
|
2246
2344
|
catch (error) {
|
|
2247
2345
|
this.onLoaderChangeWithRace(false);
|
|
@@ -2759,6 +2857,7 @@ const Billing = {
|
|
|
2759
2857
|
if (typeof window !== 'undefined') {
|
|
2760
2858
|
window.Billing = Billing;
|
|
2761
2859
|
}
|
|
2860
|
+
console.debug('Billing SDK inited');
|
|
2762
2861
|
|
|
2763
2862
|
exports.APIError = APIError;
|
|
2764
2863
|
exports.Billing = Billing;
|
package/dist/chunk-index.es.js
CHANGED
|
@@ -489,7 +489,7 @@ var PaymentMethod;
|
|
|
489
489
|
/**
|
|
490
490
|
* @fileoverview Constants for Funnefox SDK
|
|
491
491
|
*/
|
|
492
|
-
const SDK_VERSION = '0.
|
|
492
|
+
const SDK_VERSION = '0.8.0-ffb-395.14';
|
|
493
493
|
const DEFAULTS = {
|
|
494
494
|
BASE_URL: 'https://billing.funnelfox.com',
|
|
495
495
|
REGION: 'default',
|
|
@@ -530,6 +530,7 @@ const API_ENDPOINTS = {
|
|
|
530
530
|
RESUME_PAYMENT: '/v1/checkout/resume_payment',
|
|
531
531
|
ONE_CLICK: '/v1/checkout/one_click',
|
|
532
532
|
CREATE_SIMPLE_CLIENT_SESSION: '/v1/checkout/create_simple_client_session',
|
|
533
|
+
RECALCULATE_TAX: '/v1/checkout/recalculate_tax',
|
|
533
534
|
};
|
|
534
535
|
const ERROR_CODES = {
|
|
535
536
|
SDK_ERROR: 'SDK_ERROR',
|
|
@@ -1167,11 +1168,35 @@ class APIClient {
|
|
|
1167
1168
|
if (params.postalCode !== undefined) {
|
|
1168
1169
|
payload.postal_code = params.postalCode;
|
|
1169
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
|
+
}
|
|
1170
1177
|
return (await this.request(API_ENDPOINTS.CREATE_PAYMENT, {
|
|
1171
1178
|
method: 'POST',
|
|
1172
1179
|
body: JSON.stringify(payload),
|
|
1173
1180
|
}));
|
|
1174
1181
|
}
|
|
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, {
|
|
1195
|
+
method: 'POST',
|
|
1196
|
+
body: JSON.stringify(payload),
|
|
1197
|
+
}));
|
|
1198
|
+
return raw.data;
|
|
1199
|
+
}
|
|
1175
1200
|
async resumePayment(params) {
|
|
1176
1201
|
const payload = {
|
|
1177
1202
|
order_id: params.orderId,
|
|
@@ -1751,6 +1776,7 @@ function getPageUrl() {
|
|
|
1751
1776
|
/**
|
|
1752
1777
|
* @fileoverview Checkout instance manager for Funnefox SDK
|
|
1753
1778
|
*/
|
|
1779
|
+
const TAX_RECALC_DEBOUNCE_MS = 600;
|
|
1754
1780
|
class CheckoutInstance extends EventEmitter {
|
|
1755
1781
|
constructor(config) {
|
|
1756
1782
|
super();
|
|
@@ -1786,11 +1812,71 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1786
1812
|
if (!this.isPostalCodeVisible()) {
|
|
1787
1813
|
this.cardPostalCode = undefined;
|
|
1788
1814
|
}
|
|
1789
|
-
return;
|
|
1790
1815
|
}
|
|
1791
|
-
if (inputName === 'postalCode') {
|
|
1816
|
+
else if (inputName === 'postalCode') {
|
|
1792
1817
|
this.cardPostalCode = value?.trim() || undefined;
|
|
1793
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
|
+
}
|
|
1794
1880
|
};
|
|
1795
1881
|
this.handleMethodRender = (method) => {
|
|
1796
1882
|
this.emit(EVENTS.METHOD_RENDER, method);
|
|
@@ -1810,6 +1896,11 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1810
1896
|
try {
|
|
1811
1897
|
this.onLoaderChangeWithRace(true);
|
|
1812
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
|
+
}
|
|
1813
1904
|
const [radarSessionId, airwallexDeviceId] = await Promise.all([
|
|
1814
1905
|
this.cachedSessionResponse?.radarSessionId,
|
|
1815
1906
|
this.cachedSessionResponse?.airwallexDeviceId,
|
|
@@ -1927,6 +2018,9 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1927
2018
|
await this.createSession();
|
|
1928
2019
|
await this._initializePrimerCheckout();
|
|
1929
2020
|
this._setState('ready');
|
|
2021
|
+
if (this.isTaxEnabled()) {
|
|
2022
|
+
this.emitSessionTaxEstimate();
|
|
2023
|
+
}
|
|
1930
2024
|
this.startUnhandledTelemetry();
|
|
1931
2025
|
this.checkoutConfig?.onInitialized?.();
|
|
1932
2026
|
return this;
|
|
@@ -2201,6 +2295,7 @@ class CheckoutInstance extends EventEmitter {
|
|
|
2201
2295
|
wasPaymentProcessedStarted = true;
|
|
2202
2296
|
},
|
|
2203
2297
|
onTokenizeShouldStart: data => {
|
|
2298
|
+
this.activePaymentMethodType = data.paymentMethodType;
|
|
2204
2299
|
this.emit(EVENTS.ERROR, undefined);
|
|
2205
2300
|
this.emit(EVENTS.START_PURCHASE, data.paymentMethodType);
|
|
2206
2301
|
return true;
|
|
@@ -2240,6 +2335,9 @@ class CheckoutInstance extends EventEmitter {
|
|
|
2240
2335
|
await this.primerWrapper.refreshClientSession();
|
|
2241
2336
|
this.onLoaderChangeWithRace(false);
|
|
2242
2337
|
this._setState('ready');
|
|
2338
|
+
if (this.isTaxEnabled()) {
|
|
2339
|
+
this.scheduleTaxRecalc();
|
|
2340
|
+
}
|
|
2243
2341
|
}
|
|
2244
2342
|
catch (error) {
|
|
2245
2343
|
this.onLoaderChangeWithRace(false);
|
|
@@ -2757,5 +2855,6 @@ const Billing = {
|
|
|
2757
2855
|
if (typeof window !== 'undefined') {
|
|
2758
2856
|
window.Billing = Billing;
|
|
2759
2857
|
}
|
|
2858
|
+
console.debug('Billing SDK inited');
|
|
2760
2859
|
|
|
2761
2860
|
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 };
|
|
@@ -10,11 +10,16 @@
|
|
|
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
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,
|
|
@@ -31,7 +36,81 @@ async function mountStripeCardForm(element, session, params) {
|
|
|
31
36
|
},
|
|
32
37
|
terms: { card: 'never' },
|
|
33
38
|
});
|
|
34
|
-
|
|
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;
|
|
@@ -54,7 +134,10 @@ async function mountStripeCardForm(element, session, params) {
|
|
|
54
134
|
orderId: order_id,
|
|
55
135
|
paymentMethodToken: paymentMethod.id,
|
|
56
136
|
email: params.email,
|
|
57
|
-
countryCode: params.countryCode,
|
|
137
|
+
countryCode: taxEnabled ? taxAddress.country : params.countryCode,
|
|
138
|
+
postalCode: taxEnabled ? taxAddress.postalCode : undefined,
|
|
139
|
+
subdivision: taxEnabled ? taxAddress.state : undefined,
|
|
140
|
+
taxCalculationId: taxEnabled ? taxCalculationId : undefined,
|
|
58
141
|
clientMetadata: params.clientMetadata,
|
|
59
142
|
});
|
|
60
143
|
const result = params.apiClient.processPaymentResponse(raw);
|
|
@@ -8,11 +8,16 @@
|
|
|
8
8
|
import { g as getStripe } from './chunk-stripe-loader.es.js';
|
|
9
9
|
import './chunk-index.es.js';
|
|
10
10
|
|
|
11
|
+
const TAX_RECALC_DEBOUNCE_MS = 600;
|
|
11
12
|
async function mountStripeCardForm(element, session, params) {
|
|
12
13
|
const { stripe_public_key, amount, currency, order_id, is_link_enabled } = session.data;
|
|
14
|
+
// Tax flow is driven by the tenant setting (session.tax_enabled); enableTax is a legacy override.
|
|
15
|
+
const taxEnabled = session.data.tax_enabled ?? params.enableTax ?? false;
|
|
13
16
|
const stripe = await getStripe(stripe_public_key);
|
|
14
17
|
if (!stripe)
|
|
15
18
|
throw new Error('Failed to load Stripe');
|
|
19
|
+
// Remounting (e.g. picking another price) must replace the form, not stack a second one.
|
|
20
|
+
element.replaceChildren();
|
|
16
21
|
const stripeElements = stripe.elements({
|
|
17
22
|
mode: 'subscription',
|
|
18
23
|
amount,
|
|
@@ -29,7 +34,81 @@ async function mountStripeCardForm(element, session, params) {
|
|
|
29
34
|
},
|
|
30
35
|
terms: { card: 'never' },
|
|
31
36
|
});
|
|
32
|
-
|
|
37
|
+
// Tax mode mounts Stripe's Address Element (its change events are readable, so tax recalculates live
|
|
38
|
+
// as the buyer edits the address) above the Payment Element. The country is pre-filled from the
|
|
39
|
+
// detected location so tax is computed from the address (not a separate IP estimate); the resulting
|
|
40
|
+
// calculation id is attached to the PaymentIntent at payment time so Stripe records the tax.
|
|
41
|
+
let taxCalculationId;
|
|
42
|
+
let taxAddress = {};
|
|
43
|
+
// Flushed on submit so the charge always reflects the final entered address (no estimate drift).
|
|
44
|
+
let flushTaxRecalc = async () => { };
|
|
45
|
+
if (taxEnabled) {
|
|
46
|
+
const addressContainer = document.createElement('div');
|
|
47
|
+
const paymentContainer = document.createElement('div');
|
|
48
|
+
element.appendChild(addressContainer);
|
|
49
|
+
element.appendChild(paymentContainer);
|
|
50
|
+
const addressElement = stripeElements.create('address', {
|
|
51
|
+
mode: 'billing',
|
|
52
|
+
fields: { phone: 'never' },
|
|
53
|
+
...(session.data.detected_country_code
|
|
54
|
+
? {
|
|
55
|
+
defaultValues: {
|
|
56
|
+
address: { country: session.data.detected_country_code },
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
: {}),
|
|
60
|
+
});
|
|
61
|
+
addressElement.mount(addressContainer);
|
|
62
|
+
paymentElement.mount(paymentContainer);
|
|
63
|
+
const runRecalc = async () => {
|
|
64
|
+
const country = taxAddress.country;
|
|
65
|
+
if (!country)
|
|
66
|
+
return;
|
|
67
|
+
try {
|
|
68
|
+
const tax = await params.apiClient.recalculateTax({
|
|
69
|
+
orderId: order_id,
|
|
70
|
+
clientToken: session.data.client_token,
|
|
71
|
+
countryCode: country,
|
|
72
|
+
postalCode: taxAddress.postalCode,
|
|
73
|
+
subdivision: taxAddress.state,
|
|
74
|
+
});
|
|
75
|
+
taxCalculationId = tax.tax_calculation_id;
|
|
76
|
+
stripeElements.update({ amount: tax.amount_total });
|
|
77
|
+
params.onTaxChange?.({
|
|
78
|
+
amountTotal: tax.amount_total,
|
|
79
|
+
taxAmount: tax.tax_amount,
|
|
80
|
+
currency: tax.currency,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Best-effort: keep the current total if recalculation fails.
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
let debounce;
|
|
88
|
+
flushTaxRecalc = async () => {
|
|
89
|
+
if (debounce) {
|
|
90
|
+
clearTimeout(debounce);
|
|
91
|
+
debounce = undefined;
|
|
92
|
+
}
|
|
93
|
+
await runRecalc();
|
|
94
|
+
};
|
|
95
|
+
addressElement.on('change', event => {
|
|
96
|
+
const address = event.value.address;
|
|
97
|
+
taxAddress = {
|
|
98
|
+
country: address.country,
|
|
99
|
+
postalCode: address.postal_code || undefined,
|
|
100
|
+
state: address.state || undefined,
|
|
101
|
+
};
|
|
102
|
+
if (!taxAddress.country)
|
|
103
|
+
return;
|
|
104
|
+
if (debounce)
|
|
105
|
+
clearTimeout(debounce);
|
|
106
|
+
debounce = setTimeout(runRecalc, TAX_RECALC_DEBOUNCE_MS);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
paymentElement.mount(element);
|
|
111
|
+
}
|
|
33
112
|
await new Promise((resolve, reject) => {
|
|
34
113
|
paymentElement.once('ready', () => resolve());
|
|
35
114
|
// 'loaderror' is a valid Stripe event but not yet in the @stripe/stripe-js types
|
|
@@ -40,6 +119,7 @@ async function mountStripeCardForm(element, session, params) {
|
|
|
40
119
|
submit: async () => {
|
|
41
120
|
params.onLoaderChange?.(true);
|
|
42
121
|
try {
|
|
122
|
+
await flushTaxRecalc();
|
|
43
123
|
const { error: submitError } = await stripeElements.submit();
|
|
44
124
|
if (submitError)
|
|
45
125
|
throw submitError;
|
|
@@ -52,7 +132,10 @@ async function mountStripeCardForm(element, session, params) {
|
|
|
52
132
|
orderId: order_id,
|
|
53
133
|
paymentMethodToken: paymentMethod.id,
|
|
54
134
|
email: params.email,
|
|
55
|
-
countryCode: params.countryCode,
|
|
135
|
+
countryCode: taxEnabled ? taxAddress.country : params.countryCode,
|
|
136
|
+
postalCode: taxEnabled ? taxAddress.postalCode : undefined,
|
|
137
|
+
subdivision: taxEnabled ? taxAddress.state : undefined,
|
|
138
|
+
taxCalculationId: taxEnabled ? taxCalculationId : undefined,
|
|
56
139
|
clientMetadata: params.clientMetadata,
|
|
57
140
|
});
|
|
58
141
|
const result = params.apiClient.processPaymentResponse(raw);
|
|
@@ -30,13 +30,24 @@ function buildPaymentRequest(stripe, data, totalLabel) {
|
|
|
30
30
|
const applePay = raw
|
|
31
31
|
? { recurringPaymentRequest: raw }
|
|
32
32
|
: undefined;
|
|
33
|
+
const base = data.amount;
|
|
34
|
+
const tax = data.tax_amount ?? 0;
|
|
35
|
+
const total = data.amount_total ?? base;
|
|
33
36
|
return stripe.paymentRequest({
|
|
34
37
|
country: data.country,
|
|
35
38
|
currency: data.currency,
|
|
36
39
|
total: {
|
|
37
40
|
label: totalLabel?.trim() || 'Total',
|
|
38
|
-
amount:
|
|
41
|
+
amount: total,
|
|
39
42
|
},
|
|
43
|
+
// Detected-country tax. The wallet sheet amount is fixed at open (not recomputed from the address
|
|
44
|
+
// picked in the sheet), so this line is the final charged tax, shown plainly as "Tax".
|
|
45
|
+
displayItems: tax > 0
|
|
46
|
+
? [
|
|
47
|
+
{ label: 'Subtotal', amount: base },
|
|
48
|
+
{ label: 'Tax', amount: tax },
|
|
49
|
+
]
|
|
50
|
+
: undefined,
|
|
40
51
|
requestPayerName: false,
|
|
41
52
|
requestPayerEmail: false,
|
|
42
53
|
applePay,
|
|
@@ -59,6 +70,7 @@ async function getAvailableWallet(session) {
|
|
|
59
70
|
}
|
|
60
71
|
async function purchaseWallet(session, params) {
|
|
61
72
|
const { stripe_public_key, order_id } = session.data;
|
|
73
|
+
const taxEnabled = session.data.tax_enabled ?? false;
|
|
62
74
|
const stripe = await stripeLoader.getStripe(stripe_public_key);
|
|
63
75
|
if (!stripe)
|
|
64
76
|
throw new Error('Failed to load Stripe');
|
|
@@ -70,11 +82,28 @@ async function purchaseWallet(session, params) {
|
|
|
70
82
|
paymentRequest.on('paymentmethod', async (event) => {
|
|
71
83
|
params.onLoaderChange?.(true);
|
|
72
84
|
try {
|
|
85
|
+
// Tax on: charge stays the detected-country estimate (the amount the sheet authorized, via
|
|
86
|
+
// tax_calculation_id); commit the finalized tax from the card's real billing address, which
|
|
87
|
+
// Stripe only exposes on the payment method after authorization. Tax off: send only the
|
|
88
|
+
// caller-provided country, exactly as before the tax flow existed.
|
|
89
|
+
const billingAddress = taxEnabled
|
|
90
|
+
? event.paymentMethod.billing_details.address
|
|
91
|
+
: undefined;
|
|
73
92
|
const raw = await params.apiClient.createPayment({
|
|
74
93
|
orderId: order_id,
|
|
75
94
|
paymentMethodToken: event.paymentMethod.id,
|
|
76
95
|
email: params.email,
|
|
77
|
-
countryCode:
|
|
96
|
+
countryCode: taxEnabled
|
|
97
|
+
? billingAddress?.country ||
|
|
98
|
+
session.data.detected_country_code ||
|
|
99
|
+
params.countryCode
|
|
100
|
+
: params.countryCode,
|
|
101
|
+
postalCode: taxEnabled
|
|
102
|
+
? billingAddress?.postal_code || undefined
|
|
103
|
+
: undefined,
|
|
104
|
+
taxCalculationId: taxEnabled
|
|
105
|
+
? session.data.tax_calculation_id
|
|
106
|
+
: undefined,
|
|
78
107
|
clientMetadata: params.clientMetadata,
|
|
79
108
|
});
|
|
80
109
|
const result = params.apiClient.processPaymentResponse(raw);
|