@funnelfox/billing 0.9.0 → 0.9.1

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.
@@ -8,70 +8,9 @@
8
8
  'use strict';
9
9
 
10
10
  var stripeLoader = require('./chunk-stripe-loader.cjs.js');
11
- var index = require('./chunk-index.cjs.js');
11
+ require('./chunk-index.cjs.js');
12
12
 
13
- const TAX_STYLE_ID = 'ff-stripe-tax-styles';
14
13
  const TAX_RECALC_DEBOUNCE_MS = 600;
15
- // The Stripe card form renders Stripe Elements (not the card skin), so it owns its own subtotal/tax/
16
- // total summary here — mirroring the hosted card skin so every integration shows tax consistently
17
- // without the host page reimplementing it.
18
- function injectTaxStyles() {
19
- if (typeof document === 'undefined')
20
- return;
21
- if (document.getElementById(TAX_STYLE_ID))
22
- return;
23
- const style = document.createElement('style');
24
- style.id = TAX_STYLE_ID;
25
- style.textContent = [
26
- '.ff-tax-summary{margin-top:12px;padding-top:12px;border-top:1px solid rgb(0 0 0 / 10%);font-size:16px;transition:opacity .2s ease}',
27
- '.ff-tax-summary[hidden]{display:none}',
28
- '.ff-tax-summary--updating{opacity:.55}',
29
- '.ff-tax-row{display:flex;justify-content:space-between;gap:10px;margin-bottom:6px}',
30
- '.ff-tax-row.ff-tax-total{margin-top:8px;margin-bottom:0;font-weight:600}',
31
- ].join('\n');
32
- document.head.appendChild(style);
33
- }
34
- function createTaxSummary() {
35
- const root = document.createElement('div');
36
- root.className = 'ff-tax-summary';
37
- root.hidden = true;
38
- const makeRow = (label, isTotal = false) => {
39
- const rowEl = document.createElement('div');
40
- rowEl.className = isTotal ? 'ff-tax-row ff-tax-total' : 'ff-tax-row';
41
- const labelEl = document.createElement('span');
42
- labelEl.textContent = label;
43
- const valueEl = document.createElement('span');
44
- rowEl.append(labelEl, valueEl);
45
- root.appendChild(rowEl);
46
- return valueEl;
47
- };
48
- const subtotalEl = makeRow('Subtotal');
49
- const taxEl = makeRow('Tax');
50
- const totalEl = makeRow('Total', true);
51
- return {
52
- root,
53
- // taxAmount is the tax added on top of the price; it is 0 for tax-inclusive pricing and for
54
- // non-taxed locations — in both cases there is nothing to itemise, so the summary stays hidden.
55
- render(info) {
56
- root.classList.remove('ff-tax-summary--updating');
57
- if (!info || !(info.taxAmount > 0)) {
58
- root.hidden = true;
59
- return;
60
- }
61
- subtotalEl.textContent = index.formatCurrencyAmount(info.amountTotal - info.taxAmount, info.currency);
62
- taxEl.textContent = index.formatCurrencyAmount(info.taxAmount, info.currency);
63
- totalEl.textContent = index.formatCurrencyAmount(info.amountTotal, info.currency);
64
- root.hidden = false;
65
- },
66
- setPending() {
67
- if (!root.hidden)
68
- root.classList.add('ff-tax-summary--updating');
69
- },
70
- setIdle() {
71
- root.classList.remove('ff-tax-summary--updating');
72
- },
73
- };
74
- }
75
14
  async function mountStripeCardForm(element, session, params) {
76
15
  const { stripe_public_key, amount, currency, order_id, is_link_enabled, tax_amount, amount_total, } = session.data;
77
16
  // Tax flow is driven by the tenant setting (session.tax_enabled); enableTax is a legacy override.
@@ -81,8 +20,6 @@ async function mountStripeCardForm(element, session, params) {
81
20
  throw new Error('Failed to load Stripe');
82
21
  // Remounting (e.g. picking another price) must replace the form, not stack a second one.
83
22
  element.replaceChildren();
84
- injectTaxStyles();
85
- const taxSummary = createTaxSummary();
86
23
  const stripeElements = stripe.elements({
87
24
  mode: 'subscription',
88
25
  amount,
@@ -105,21 +42,22 @@ async function mountStripeCardForm(element, session, params) {
105
42
  terms: { card: 'never' },
106
43
  fields: {
107
44
  billingDetails: {
108
- // In tax mode collect only what the org configured (country and/or postal) and hide the rest
109
- // of the address; otherwise use Stripe's default card billing (country + postal).
110
- address: taxEnabled
111
- ? {
112
- line1: 'never',
113
- line2: 'never',
114
- city: 'never',
115
- state: 'never',
116
- country: showCountry ? 'auto' : 'never',
117
- postalCode: collectLocation ? 'auto' : 'never',
118
- }
119
- : 'auto',
45
+ // Field visibility is owned by the org settings (Taxes → Settings) and is independent of
46
+ // whether tax is enabled: show the country selector and/or postal code only when configured,
47
+ // and always hide the rest of the address (supplied to createPaymentMethod below). Previously
48
+ // the untaxed path fell back to Stripe's default billing (country + postal), showing those
49
+ // fields even when the org had turned them off.
50
+ address: {
51
+ line1: 'never',
52
+ line2: 'never',
53
+ city: 'never',
54
+ state: 'never',
55
+ country: showCountry ? 'auto' : 'never',
56
+ postalCode: showPostal ? 'auto' : 'never',
57
+ },
120
58
  },
121
59
  },
122
- ...(session.data.detected_country_code && (!taxEnabled || showCountry)
60
+ ...(session.data.detected_country_code && showCountry
123
61
  ? {
124
62
  defaultValues: {
125
63
  billingDetails: {
@@ -130,16 +68,16 @@ async function mountStripeCardForm(element, session, params) {
130
68
  : {}),
131
69
  });
132
70
  // Address fields we hid (fields=never) must still be supplied to createPaymentMethod. Only country +
133
- // postal matter for tax; country is hidden only when the selector is off, postal only when we collect
134
- // no location at all.
71
+ // postal matter for tax; country is supplied when its selector is off, postal when its field is off.
135
72
  const hiddenBillingAddress = { line1: '', line2: '', city: '', state: '' };
136
73
  if (!showCountry)
137
74
  hiddenBillingAddress.country = session.data.detected_country_code ?? '';
138
- if (!collectLocation)
75
+ if (!showPostal)
139
76
  hiddenBillingAddress.postal_code = '';
140
- // Tax needs only the country + postal code. When the org collects location on the form we read it from
141
- // the Payment Element's change events as the buyer types and recalculate tax live, then attach the
142
- // calculation id to the charge. When it does not, tax follows the IP-derived session estimate.
77
+ // Tax needs only the country + postal code. When the org collects location on the form we read it
78
+ // from the Payment Element's change events as the buyer types, recalculate tax live (reported via
79
+ // onTaxChange), and attach the calculation id to the charge. When it does not, tax follows the
80
+ // IP-derived session estimate.
143
81
  let taxCalc;
144
82
  let recalcSeq = 0;
145
83
  if (taxEnabled && session.data.tax_calculation_id) {
@@ -155,7 +93,6 @@ async function mountStripeCardForm(element, session, params) {
155
93
  const country = taxAddress.country;
156
94
  const postal = taxAddress.postalCode;
157
95
  if (!country) {
158
- taxSummary.setIdle();
159
96
  return;
160
97
  }
161
98
  const seq = ++recalcSeq;
@@ -169,13 +106,11 @@ async function mountStripeCardForm(element, session, params) {
169
106
  if (seq !== recalcSeq)
170
107
  return; // a newer recalc superseded this one
171
108
  taxCalc = { id: tax.tax_calculation_id, country, postal };
172
- const info = {
109
+ params.onTaxChange?.({
173
110
  amountTotal: tax.amount_total,
174
111
  taxAmount: tax.tax_amount,
175
112
  currency: tax.currency,
176
- };
177
- taxSummary.render(info);
178
- params.onTaxChange?.(info);
113
+ });
179
114
  }
180
115
  catch (err) {
181
116
  if (seq !== recalcSeq)
@@ -184,7 +119,6 @@ async function mountStripeCardForm(element, session, params) {
184
119
  taxCalc = undefined;
185
120
  params.onTaxError?.(err);
186
121
  }
187
- taxSummary.setIdle();
188
122
  }
189
123
  };
190
124
  paymentElement.mount(element);
@@ -209,19 +143,21 @@ async function mountStripeCardForm(element, session, params) {
209
143
  };
210
144
  if (!taxAddress.country)
211
145
  return;
212
- taxSummary.setPending();
213
146
  if (debounce)
214
147
  clearTimeout(debounce);
215
148
  debounce = setTimeout(runRecalc, TAX_RECALC_DEBOUNCE_MS);
216
149
  });
217
150
  }
218
- // SDK-owned summary lives below the form and reflects the session's tax estimate immediately; when the
219
- // org collects location, the Payment Element's fields then keep it in sync as the buyer edits them.
220
- element.appendChild(taxSummary.root);
221
- taxSummary.render(amount_total != null
222
- ? { amountTotal: amount_total, taxAmount: tax_amount ?? 0, currency }
223
- : null);
224
- // Refine the initial estimate against the pre-filled country right away.
151
+ // The form renders no tax lines; the host re-renders its own prices off onTaxChange. Report the
152
+ // session estimate as the first calculation, then (when the org collects location) refine it
153
+ // against the pre-filled country right away — later address edits keep firing it via the recalc.
154
+ if (taxEnabled && amount_total != null) {
155
+ params.onTaxChange?.({
156
+ amountTotal: amount_total,
157
+ taxAmount: tax_amount ?? 0,
158
+ currency,
159
+ });
160
+ }
225
161
  if (taxEnabled && collectLocation)
226
162
  void runRecalc();
227
163
  await new Promise((resolve, reject) => {
@@ -243,10 +179,10 @@ async function mountStripeCardForm(element, session, params) {
243
179
  const { error, paymentMethod } = await stripe.createPaymentMethod({
244
180
  elements: stripeElements,
245
181
  // Supply the address fields we hid on the Payment Element (fields=never); Stripe requires
246
- // them here. Country/postal collected by the element are filled by Stripe automatically.
247
- ...(taxEnabled
248
- ? { params: { billing_details: { address: hiddenBillingAddress } } }
249
- : {}),
182
+ // them here. We always hide line1/line2/city/state (and country/postal per the org
183
+ // settings), so these must be supplied regardless of tax mode. Country/postal shown on the
184
+ // element are filled by Stripe automatically.
185
+ params: { billing_details: { address: hiddenBillingAddress } },
250
186
  });
251
187
  if (error)
252
188
  throw error;
@@ -6,70 +6,9 @@
6
6
  * @license MIT
7
7
  */
8
8
  import { g as getStripe } from './chunk-stripe-loader.es.js';
9
- import { f as formatCurrencyAmount } from './chunk-index.es.js';
9
+ import './chunk-index.es.js';
10
10
 
11
- const TAX_STYLE_ID = 'ff-stripe-tax-styles';
12
11
  const TAX_RECALC_DEBOUNCE_MS = 600;
13
- // The Stripe card form renders Stripe Elements (not the card skin), so it owns its own subtotal/tax/
14
- // total summary here — mirroring the hosted card skin so every integration shows tax consistently
15
- // without the host page reimplementing it.
16
- function injectTaxStyles() {
17
- if (typeof document === 'undefined')
18
- return;
19
- if (document.getElementById(TAX_STYLE_ID))
20
- return;
21
- const style = document.createElement('style');
22
- style.id = TAX_STYLE_ID;
23
- style.textContent = [
24
- '.ff-tax-summary{margin-top:12px;padding-top:12px;border-top:1px solid rgb(0 0 0 / 10%);font-size:16px;transition:opacity .2s ease}',
25
- '.ff-tax-summary[hidden]{display:none}',
26
- '.ff-tax-summary--updating{opacity:.55}',
27
- '.ff-tax-row{display:flex;justify-content:space-between;gap:10px;margin-bottom:6px}',
28
- '.ff-tax-row.ff-tax-total{margin-top:8px;margin-bottom:0;font-weight:600}',
29
- ].join('\n');
30
- document.head.appendChild(style);
31
- }
32
- function createTaxSummary() {
33
- const root = document.createElement('div');
34
- root.className = 'ff-tax-summary';
35
- root.hidden = true;
36
- const makeRow = (label, isTotal = false) => {
37
- const rowEl = document.createElement('div');
38
- rowEl.className = isTotal ? 'ff-tax-row ff-tax-total' : 'ff-tax-row';
39
- const labelEl = document.createElement('span');
40
- labelEl.textContent = label;
41
- const valueEl = document.createElement('span');
42
- rowEl.append(labelEl, valueEl);
43
- root.appendChild(rowEl);
44
- return valueEl;
45
- };
46
- const subtotalEl = makeRow('Subtotal');
47
- const taxEl = makeRow('Tax');
48
- const totalEl = makeRow('Total', true);
49
- return {
50
- root,
51
- // taxAmount is the tax added on top of the price; it is 0 for tax-inclusive pricing and for
52
- // non-taxed locations — in both cases there is nothing to itemise, so the summary stays hidden.
53
- render(info) {
54
- root.classList.remove('ff-tax-summary--updating');
55
- if (!info || !(info.taxAmount > 0)) {
56
- root.hidden = true;
57
- return;
58
- }
59
- subtotalEl.textContent = formatCurrencyAmount(info.amountTotal - info.taxAmount, info.currency);
60
- taxEl.textContent = formatCurrencyAmount(info.taxAmount, info.currency);
61
- totalEl.textContent = formatCurrencyAmount(info.amountTotal, info.currency);
62
- root.hidden = false;
63
- },
64
- setPending() {
65
- if (!root.hidden)
66
- root.classList.add('ff-tax-summary--updating');
67
- },
68
- setIdle() {
69
- root.classList.remove('ff-tax-summary--updating');
70
- },
71
- };
72
- }
73
12
  async function mountStripeCardForm(element, session, params) {
74
13
  const { stripe_public_key, amount, currency, order_id, is_link_enabled, tax_amount, amount_total, } = session.data;
75
14
  // Tax flow is driven by the tenant setting (session.tax_enabled); enableTax is a legacy override.
@@ -79,8 +18,6 @@ async function mountStripeCardForm(element, session, params) {
79
18
  throw new Error('Failed to load Stripe');
80
19
  // Remounting (e.g. picking another price) must replace the form, not stack a second one.
81
20
  element.replaceChildren();
82
- injectTaxStyles();
83
- const taxSummary = createTaxSummary();
84
21
  const stripeElements = stripe.elements({
85
22
  mode: 'subscription',
86
23
  amount,
@@ -103,21 +40,22 @@ async function mountStripeCardForm(element, session, params) {
103
40
  terms: { card: 'never' },
104
41
  fields: {
105
42
  billingDetails: {
106
- // In tax mode collect only what the org configured (country and/or postal) and hide the rest
107
- // of the address; otherwise use Stripe's default card billing (country + postal).
108
- address: taxEnabled
109
- ? {
110
- line1: 'never',
111
- line2: 'never',
112
- city: 'never',
113
- state: 'never',
114
- country: showCountry ? 'auto' : 'never',
115
- postalCode: collectLocation ? 'auto' : 'never',
116
- }
117
- : 'auto',
43
+ // Field visibility is owned by the org settings (Taxes → Settings) and is independent of
44
+ // whether tax is enabled: show the country selector and/or postal code only when configured,
45
+ // and always hide the rest of the address (supplied to createPaymentMethod below). Previously
46
+ // the untaxed path fell back to Stripe's default billing (country + postal), showing those
47
+ // fields even when the org had turned them off.
48
+ address: {
49
+ line1: 'never',
50
+ line2: 'never',
51
+ city: 'never',
52
+ state: 'never',
53
+ country: showCountry ? 'auto' : 'never',
54
+ postalCode: showPostal ? 'auto' : 'never',
55
+ },
118
56
  },
119
57
  },
120
- ...(session.data.detected_country_code && (!taxEnabled || showCountry)
58
+ ...(session.data.detected_country_code && showCountry
121
59
  ? {
122
60
  defaultValues: {
123
61
  billingDetails: {
@@ -128,16 +66,16 @@ async function mountStripeCardForm(element, session, params) {
128
66
  : {}),
129
67
  });
130
68
  // Address fields we hid (fields=never) must still be supplied to createPaymentMethod. Only country +
131
- // postal matter for tax; country is hidden only when the selector is off, postal only when we collect
132
- // no location at all.
69
+ // postal matter for tax; country is supplied when its selector is off, postal when its field is off.
133
70
  const hiddenBillingAddress = { line1: '', line2: '', city: '', state: '' };
134
71
  if (!showCountry)
135
72
  hiddenBillingAddress.country = session.data.detected_country_code ?? '';
136
- if (!collectLocation)
73
+ if (!showPostal)
137
74
  hiddenBillingAddress.postal_code = '';
138
- // Tax needs only the country + postal code. When the org collects location on the form we read it from
139
- // the Payment Element's change events as the buyer types and recalculate tax live, then attach the
140
- // calculation id to the charge. When it does not, tax follows the IP-derived session estimate.
75
+ // Tax needs only the country + postal code. When the org collects location on the form we read it
76
+ // from the Payment Element's change events as the buyer types, recalculate tax live (reported via
77
+ // onTaxChange), and attach the calculation id to the charge. When it does not, tax follows the
78
+ // IP-derived session estimate.
141
79
  let taxCalc;
142
80
  let recalcSeq = 0;
143
81
  if (taxEnabled && session.data.tax_calculation_id) {
@@ -153,7 +91,6 @@ async function mountStripeCardForm(element, session, params) {
153
91
  const country = taxAddress.country;
154
92
  const postal = taxAddress.postalCode;
155
93
  if (!country) {
156
- taxSummary.setIdle();
157
94
  return;
158
95
  }
159
96
  const seq = ++recalcSeq;
@@ -167,13 +104,11 @@ async function mountStripeCardForm(element, session, params) {
167
104
  if (seq !== recalcSeq)
168
105
  return; // a newer recalc superseded this one
169
106
  taxCalc = { id: tax.tax_calculation_id, country, postal };
170
- const info = {
107
+ params.onTaxChange?.({
171
108
  amountTotal: tax.amount_total,
172
109
  taxAmount: tax.tax_amount,
173
110
  currency: tax.currency,
174
- };
175
- taxSummary.render(info);
176
- params.onTaxChange?.(info);
111
+ });
177
112
  }
178
113
  catch (err) {
179
114
  if (seq !== recalcSeq)
@@ -182,7 +117,6 @@ async function mountStripeCardForm(element, session, params) {
182
117
  taxCalc = undefined;
183
118
  params.onTaxError?.(err);
184
119
  }
185
- taxSummary.setIdle();
186
120
  }
187
121
  };
188
122
  paymentElement.mount(element);
@@ -207,19 +141,21 @@ async function mountStripeCardForm(element, session, params) {
207
141
  };
208
142
  if (!taxAddress.country)
209
143
  return;
210
- taxSummary.setPending();
211
144
  if (debounce)
212
145
  clearTimeout(debounce);
213
146
  debounce = setTimeout(runRecalc, TAX_RECALC_DEBOUNCE_MS);
214
147
  });
215
148
  }
216
- // SDK-owned summary lives below the form and reflects the session's tax estimate immediately; when the
217
- // org collects location, the Payment Element's fields then keep it in sync as the buyer edits them.
218
- element.appendChild(taxSummary.root);
219
- taxSummary.render(amount_total != null
220
- ? { amountTotal: amount_total, taxAmount: tax_amount ?? 0, currency }
221
- : null);
222
- // Refine the initial estimate against the pre-filled country right away.
149
+ // The form renders no tax lines; the host re-renders its own prices off onTaxChange. Report the
150
+ // session estimate as the first calculation, then (when the org collects location) refine it
151
+ // against the pre-filled country right away — later address edits keep firing it via the recalc.
152
+ if (taxEnabled && amount_total != null) {
153
+ params.onTaxChange?.({
154
+ amountTotal: amount_total,
155
+ taxAmount: tax_amount ?? 0,
156
+ currency,
157
+ });
158
+ }
223
159
  if (taxEnabled && collectLocation)
224
160
  void runRecalc();
225
161
  await new Promise((resolve, reject) => {
@@ -241,10 +177,10 @@ async function mountStripeCardForm(element, session, params) {
241
177
  const { error, paymentMethod } = await stripe.createPaymentMethod({
242
178
  elements: stripeElements,
243
179
  // Supply the address fields we hid on the Payment Element (fields=never); Stripe requires
244
- // them here. Country/postal collected by the element are filled by Stripe automatically.
245
- ...(taxEnabled
246
- ? { params: { billing_details: { address: hiddenBillingAddress } } }
247
- : {}),
180
+ // them here. We always hide line1/line2/city/state (and country/postal per the org
181
+ // settings), so these must be supplied regardless of tax mode. Country/postal shown on the
182
+ // element are filled by Stripe automatically.
183
+ params: { billing_details: { address: hiddenBillingAddress } },
248
184
  });
249
185
  if (error)
250
186
  throw error;
@@ -5,4 +5,4 @@
5
5
  * @author Funnelfox
6
6
  * @license MIT
7
7
  */
8
- export { A as APIError, B as Billing, d as CHECKOUT_STATES, C as CheckoutError, b as ConfigurationError, c as DEFAULTS, e as ERROR_CODES, E as EVENTS, F as FunnefoxSDKError, N as NetworkError, P as PaymentMethod, a as PrimerError, S as SDK_VERSION, V as ValidationError, g as configure, h as createCheckout, i as createClientSession, B as default, j as getAvailablePaymentMethods } from './chunk-index.es.js';
8
+ export { A as APIError, B as Billing, d as CHECKOUT_STATES, C as CheckoutError, b as ConfigurationError, c as DEFAULTS, e as ERROR_CODES, E as EVENTS, F as FunnefoxSDKError, N as NetworkError, P as PaymentMethod, a as PrimerError, S as SDK_VERSION, V as ValidationError, f as configure, g as createCheckout, h as createClientSession, B as default, i as getAvailablePaymentMethods } from './chunk-index.es.js';