@funnelfox/billing 0.8.0-ffb-395.10 → 0.8.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.
package/README.md CHANGED
@@ -11,7 +11,6 @@ A modern TypeScript SDK for subscription payments with Primer Headless Checkout
11
11
  - 🔧 **Robust**: Built-in error handling, retries, and validation
12
12
  - 📦 **Lightweight**: Minimal dependencies, browser-optimized
13
13
  - 🎨 **Headless Checkout**: Full control over checkout UI with Primer Headless Checkout
14
- - 💳 **Stripe Integration**: Native Stripe Elements card form and Apple Pay / Google Pay wallets
15
14
 
16
15
  ## Installation
17
16
 
@@ -712,216 +711,6 @@ const headlessCheckout = await Primer.createHeadless(session.clientToken, {
712
711
  await headlessCheckout.start();
713
712
  ```
714
713
 
715
- ## Stripe Integration
716
-
717
- The `Billing.stripe` namespace provides a Stripe-native checkout experience — no Primer dependency required. It supports card payments via Stripe Elements and native wallet payments (Apple Pay / Google Pay) via the Payment Request API.
718
-
719
- > **Note:** `@primer-io/checkout-web` is **not** required for Stripe integration. Only `@funnelfox/billing` and a Stripe-enabled price in your Funnelfox account are needed.
720
-
721
- ---
722
-
723
- ### `Billing.stripe.createCardForm(element, params)`
724
-
725
- Mounts a Stripe Elements payment form into a DOM element. Returns a `{ submit() }` handle — you control when payment is triggered (e.g. on your own button click).
726
-
727
- ```javascript
728
- const element = document.getElementById('card-form');
729
-
730
- const cardForm = await Billing.stripe.createCardForm(element, {
731
- // Required
732
- priceId: 'price_123',
733
- externalId: 'user_456',
734
-
735
- // Optional
736
- orgId: 'your-org-id',
737
- email: 'user@example.com',
738
- countryCode: 'US',
739
- showWallets: false, // show Apple Pay / Google Pay inside the form
740
- appearance: {
741
- // Stripe Elements appearance API
742
- theme: 'stripe',
743
- },
744
-
745
- // Callbacks
746
- onRenderSuccess: () => {
747
- document.getElementById('pay-button').disabled = false;
748
- },
749
- onLoaderChange: loading => {
750
- document.getElementById('pay-button').disabled = loading;
751
- },
752
- onPaymentSuccess: (paymentMethod, orderId) => {
753
- window.location.href = '/success?order=' + orderId;
754
- },
755
- onPaymentFail: error => {
756
- console.error('Payment failed:', error.message);
757
- },
758
- });
759
-
760
- // Wire up your own submit button
761
- document.getElementById('pay-button').addEventListener('click', async () => {
762
- await cardForm.submit();
763
- });
764
- ```
765
-
766
- **Key parameters:**
767
-
768
- | Parameter | Type | Description |
769
- | ---------------- | -------- | --------------------------------------------------------------------------------- |
770
- | `priceId` | string | Price identifier |
771
- | `externalId` | string | Your user identifier |
772
- | `email` | string? | Customer email |
773
- | `orgId` | string? | Org ID (if not globally configured) |
774
- | `showWallets` | boolean? | Show Apple Pay / Google Pay inside the Stripe form |
775
- | `appearance` | object? | [Stripe Elements Appearance API](https://stripe.com/docs/elements/appearance-api) |
776
- | `clientMetadata` | object? | Custom metadata attached to the order |
777
-
778
- **Returns:** `Promise<{ submit: () => Promise<void> }>`
779
-
780
- ---
781
-
782
- ### `Billing.stripe.getAvailableWallet(params)`
783
-
784
- Checks whether Apple Pay or Google Pay is available on the current device and browser. Use this to conditionally show a wallet button before attempting payment.
785
-
786
- ```javascript
787
- const wallet = await Billing.stripe.getAvailableWallet({
788
- priceId: 'price_123',
789
- externalId: 'user_456',
790
- });
791
-
792
- if (wallet === 'APPLE_PAY') {
793
- document.getElementById('apple-pay-btn').style.display = 'block';
794
- } else if (wallet === 'GOOGLE_PAY') {
795
- document.getElementById('google-pay-btn').style.display = 'block';
796
- } else {
797
- // No wallet available — show card form only
798
- }
799
- ```
800
-
801
- **Returns:** `Promise<'APPLE_PAY' | 'GOOGLE_PAY' | null>`
802
-
803
- ---
804
-
805
- ### `Billing.stripe.purchaseWallet(params)`
806
-
807
- Triggers the native Apple Pay or Google Pay payment sheet. Call this on button click after confirming a wallet is available via `getAvailableWallet`.
808
-
809
- ```javascript
810
- document.getElementById('wallet-btn').addEventListener('click', async () => {
811
- await Billing.stripe.purchaseWallet({
812
- priceId: 'price_123',
813
- externalId: 'user_456',
814
- totalLabel: 'Premium Plan', // Label shown in the payment sheet
815
-
816
- onPaymentSuccess: (paymentMethod, orderId) => {
817
- window.location.href = '/success?order=' + orderId;
818
- },
819
- onPaymentFail: error => {
820
- console.error('Wallet payment failed:', error.message);
821
- },
822
- onPaymentCancel: () => {
823
- console.log('User cancelled');
824
- },
825
- onLoaderChange: loading => {
826
- document.getElementById('wallet-btn').disabled = loading;
827
- },
828
- });
829
- });
830
- ```
831
-
832
- **Key parameters:**
833
-
834
- | Parameter | Type | Description |
835
- | ---------------- | ------- | --------------------------------------------------- |
836
- | `priceId` | string | Price identifier |
837
- | `externalId` | string | Your user identifier |
838
- | `totalLabel` | string? | Label shown next to the amount in the payment sheet |
839
- | `email` | string? | Customer email |
840
- | `clientMetadata` | object? | Custom metadata attached to the order |
841
-
842
- ---
843
-
844
- ### `Billing.stripe.getAvailablePaymentMethods(params)`
845
-
846
- Returns all Stripe payment methods available for the current device. Always includes `PAYMENT_CARD`; also includes a wallet method if one is detected.
847
-
848
- ```javascript
849
- const methods = await Billing.stripe.getAvailablePaymentMethods({
850
- priceId: 'price_123',
851
- externalId: 'user_456',
852
- });
853
-
854
- // methods: ['PAYMENT_CARD', 'APPLE_PAY'] or ['PAYMENT_CARD'] etc.
855
- console.log('Available:', methods);
856
- ```
857
-
858
- **Returns:** `Promise<PaymentMethod[]>` — always contains `PAYMENT_CARD`, optionally `APPLE_PAY` or `GOOGLE_PAY`
859
-
860
- ---
861
-
862
- ### Combined Example: Wallet Detection + Card Form Fallback
863
-
864
- The recommended pattern — show a wallet button when available, always show the card form as fallback:
865
-
866
- ```javascript
867
- import { Billing } from '@funnelfox/billing';
868
-
869
- Billing.configure({ orgId: 'your-org-id' });
870
-
871
- const params = {
872
- priceId: 'price_123',
873
- externalId: 'user_456',
874
- email: 'user@example.com',
875
- };
876
-
877
- async function initCheckout() {
878
- // 1. Check for wallet availability
879
- const wallet = await Billing.stripe.getAvailableWallet(params);
880
-
881
- if (wallet) {
882
- const walletBtn = document.getElementById('wallet-btn');
883
- walletBtn.textContent =
884
- wallet === 'APPLE_PAY' ? 'Pay with Apple Pay' : 'Pay with Google Pay';
885
- walletBtn.style.display = 'block';
886
-
887
- walletBtn.addEventListener('click', () => {
888
- Billing.stripe.purchaseWallet({
889
- ...params,
890
- totalLabel: 'Premium Plan',
891
- onPaymentSuccess: (_, orderId) => {
892
- window.location.href = '/success?order=' + orderId;
893
- },
894
- onPaymentFail: err => alert(err.message),
895
- onPaymentCancel: () => console.log('Cancelled'),
896
- });
897
- });
898
- }
899
-
900
- // 2. Always mount card form as fallback
901
- const cardForm = await Billing.stripe.createCardForm(
902
- document.getElementById('card-form'),
903
- {
904
- ...params,
905
- onPaymentSuccess: (_, orderId) => {
906
- window.location.href = '/success?order=' + orderId;
907
- },
908
- onPaymentFail: err => alert(err.message),
909
- onLoaderChange: loading => {
910
- document.getElementById('pay-btn').disabled = loading;
911
- },
912
- }
913
- );
914
-
915
- document.getElementById('pay-btn').addEventListener('click', () => {
916
- cardForm.submit();
917
- });
918
- }
919
-
920
- initCheckout();
921
- ```
922
-
923
- ---
924
-
925
714
  ## Browser Support
926
715
 
927
716
  - Chrome 60+
@@ -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.1';
494
+ const SDK_VERSION = '0.8.1';
495
495
  const DEFAULTS = {
496
496
  BASE_URL: 'https://billing.funnelfox.com',
497
497
  REGION: 'default',
@@ -1128,7 +1128,7 @@ class APIClient {
1128
1128
  async createClientSession(params) {
1129
1129
  const payload = {
1130
1130
  region: params.region || 'default',
1131
- integration_type: params.integration ?? 'primer',
1131
+ integration_type: 'primer',
1132
1132
  pp_ident: params.priceId,
1133
1133
  external_id: params.externalId,
1134
1134
  email_address: params.email,
@@ -1264,45 +1264,6 @@ class APIClient {
1264
1264
  }
1265
1265
  }
1266
1266
 
1267
- class SessionService {
1268
- constructor() {
1269
- this.cache = new Map();
1270
- }
1271
- buildCacheKey(p) {
1272
- return [p.orgId, p.priceId, p.externalId, p.email, p.integration].join('-');
1273
- }
1274
- makeClient(orgId, baseUrl) {
1275
- return new APIClient({
1276
- baseUrl: baseUrl || DEFAULTS.BASE_URL,
1277
- orgId,
1278
- timeout: DEFAULTS.REQUEST_TIMEOUT,
1279
- retryAttempts: DEFAULTS.RETRY_ATTEMPTS,
1280
- });
1281
- }
1282
- createSession(p) {
1283
- const key = this.buildCacheKey(p);
1284
- const cached = this.cache.get(key);
1285
- if (cached)
1286
- return cached;
1287
- const client = this.makeClient(p.orgId, p.baseUrl);
1288
- const req = client.createClientSession({
1289
- priceId: p.priceId,
1290
- externalId: p.externalId,
1291
- email: p.email,
1292
- region: p.region || DEFAULTS.REGION,
1293
- clientMetadata: p.clientMetadata,
1294
- countryCode: p.countryCode,
1295
- integration: p.integration,
1296
- });
1297
- this.cache.set(key, req);
1298
- return req;
1299
- }
1300
- clearCache() {
1301
- this.cache.clear();
1302
- }
1303
- }
1304
- var sessionService = new SessionService();
1305
-
1306
1267
  var loaderHtml = "<div class=\"ff-sdk-loader-container\">\n <div class=\"ff-sdk-loader\"></div>\n</div>\n";
1307
1268
 
1308
1269
  if(typeof document!=="undefined")document.head.appendChild(document.createElement("style")).textContent=".ff-sdk-loader-container {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n background-color: rgba(255, 255, 255);\n z-index: 2;\n}\n\n.ff-sdk-loader {\n width: 24px;\n height: 24px;\n border: 4px solid #e32f41;\n border-top: 4px solid transparent;\n border-radius: 50%;\n animation: spin 1s linear infinite;\n}\n\n@keyframes spin {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n }";
@@ -1897,12 +1858,6 @@ class CheckoutInstance extends EventEmitter {
1897
1858
  this.cardEmailAddress = this.checkoutConfig.customer.email;
1898
1859
  this.shouldApplySessionCardholderNameConfig =
1899
1860
  this.checkoutConfig.card?.cardholderName?.required === undefined;
1900
- this.apiClient = new APIClient({
1901
- baseUrl: this.baseUrl || DEFAULTS.BASE_URL,
1902
- orgId: this.orgId,
1903
- timeout: DEFAULTS.REQUEST_TIMEOUT,
1904
- retryAttempts: DEFAULTS.RETRY_ATTEMPTS,
1905
- });
1906
1861
  this._setupCallbackBridges();
1907
1862
  }
1908
1863
  _setupCallbackBridges() {
@@ -1944,35 +1899,64 @@ class CheckoutInstance extends EventEmitter {
1944
1899
  }
1945
1900
  }
1946
1901
  async createSession() {
1947
- const response = await sessionService.createSession({
1902
+ this.apiClient = new APIClient({
1903
+ baseUrl: this.baseUrl || DEFAULTS.BASE_URL,
1948
1904
  orgId: this.orgId,
1949
- baseUrl: this.baseUrl,
1905
+ timeout: DEFAULTS.REQUEST_TIMEOUT,
1906
+ retryAttempts: DEFAULTS.RETRY_ATTEMPTS,
1907
+ });
1908
+ const sessionParams = {
1950
1909
  priceId: this.checkoutConfig.priceId,
1951
1910
  externalId: this.checkoutConfig.customer.externalId,
1952
1911
  email: this.checkoutConfig.customer.email,
1953
- region: this.region,
1912
+ region: this.region || DEFAULTS.REGION,
1954
1913
  clientMetadata: this.checkoutConfig.clientMetadata,
1955
1914
  countryCode: this.checkoutConfig.customer.countryCode,
1956
- integration: 'primer',
1957
- });
1958
- const sessionResponse = response;
1959
- if (response.data?.stripe_public_key) {
1960
- const stripePublicKey = response.data.stripe_public_key;
1961
- sessionResponse.radarSessionId = loadStripe(stripePublicKey)
1962
- .then(stripe => stripe
1963
- ? stripe
1964
- .createRadarSession()
1965
- .then(session => session?.radarSession?.id || '')
1966
- .catch(() => '')
1967
- : '')
1968
- .catch(() => '');
1969
- }
1970
- if (response.data?.airwallex_risk_enabled) {
1971
- const isLivemode = response.data?.is_livemode;
1972
- const deviceId = generateUUID();
1973
- sessionResponse.airwallexDeviceId = loadAirwallexDeviceFingerprint(deviceId, isLivemode)
1974
- .then(() => deviceId)
1975
- .catch(() => deviceId);
1915
+ };
1916
+ const cacheKey = [
1917
+ this.orgId,
1918
+ this.checkoutConfig.priceId,
1919
+ this.checkoutConfig.customer.externalId,
1920
+ this.checkoutConfig.customer.email,
1921
+ ].join('-');
1922
+ let sessionResponse;
1923
+ // Return cached response if payload hasn't changed
1924
+ const cachedResponse = CheckoutInstance.sessionCache.get(cacheKey);
1925
+ if (cachedResponse) {
1926
+ sessionResponse = await cachedResponse;
1927
+ }
1928
+ else {
1929
+ const sessionRequest = this.apiClient
1930
+ .createClientSession(sessionParams)
1931
+ .then((response) => {
1932
+ const cachedResponse = response;
1933
+ if (response.data?.stripe_public_key) {
1934
+ const stripePublicKey = response.data.stripe_public_key;
1935
+ cachedResponse.radarSessionId = loadStripe(stripePublicKey)
1936
+ .then(stripe => stripe
1937
+ ? stripe
1938
+ .createRadarSession()
1939
+ .then(session => session?.radarSession?.id || '')
1940
+ .catch(() => '')
1941
+ : '')
1942
+ .catch(() => '');
1943
+ }
1944
+ // Initialize Airwallex device fingerprinting if enabled by backend
1945
+ if (response.data?.airwallex_risk_enabled) {
1946
+ const isLivemode = response.data?.is_livemode;
1947
+ const deviceId = generateUUID();
1948
+ cachedResponse.airwallexDeviceId = loadAirwallexDeviceFingerprint(deviceId, isLivemode)
1949
+ .then(() => deviceId)
1950
+ .catch(() => {
1951
+ // Silently fail - return deviceId anyway
1952
+ return deviceId;
1953
+ });
1954
+ }
1955
+ return cachedResponse;
1956
+ });
1957
+ // Cache the successful response
1958
+ CheckoutInstance.sessionCache.set(cacheKey, sessionRequest);
1959
+ sessionResponse = await sessionRequest;
1976
1960
  }
1977
1961
  this.cachedSessionResponse = sessionResponse;
1978
1962
  this.isTelemetryEnabled = !!sessionResponse.data?.sdk_telemetry_enabled;
@@ -2231,7 +2215,8 @@ class CheckoutInstance extends EventEmitter {
2231
2215
  try {
2232
2216
  this.onLoaderChangeWithRace(true);
2233
2217
  this._setState('updating');
2234
- sessionService.clearCache();
2218
+ // Invalidate session cache
2219
+ CheckoutInstance.sessionCache.clear();
2235
2220
  await this.apiClient.updateClientSession({
2236
2221
  orderId: this.orderId,
2237
2222
  clientToken: this.clientToken,
@@ -2264,7 +2249,7 @@ class CheckoutInstance extends EventEmitter {
2264
2249
  return;
2265
2250
  try {
2266
2251
  this.stopUnhandledTelemetry();
2267
- sessionService.clearCache();
2252
+ CheckoutInstance.sessionCache.clear();
2268
2253
  await this.primerWrapper.destroy();
2269
2254
  this._setState('destroyed');
2270
2255
  this.orderId = null;
@@ -2475,6 +2460,7 @@ class CheckoutInstance extends EventEmitter {
2475
2460
  this.telemetryPaymentMethod = undefined;
2476
2461
  }
2477
2462
  }
2463
+ CheckoutInstance.sessionCache = new Map();
2478
2464
 
2479
2465
  /**
2480
2466
  * @fileoverview Public API with configuration and orchestration logic
@@ -2665,79 +2651,6 @@ async function getAvailablePaymentMethods(params) {
2665
2651
  throw error;
2666
2652
  }
2667
2653
  }
2668
- async function createStripeCardForm(element, params) {
2669
- const config = resolveConfig(params, 'createStripeCardForm');
2670
- const [session, { mountStripeCardForm }] = await Promise.all([
2671
- sessionService.createSession({
2672
- orgId: config.orgId,
2673
- baseUrl: config.baseUrl,
2674
- region: config.region,
2675
- priceId: params.priceId,
2676
- externalId: params.externalId,
2677
- email: params.email,
2678
- clientMetadata: params.clientMetadata,
2679
- countryCode: params.countryCode,
2680
- integration: 'stripe',
2681
- }),
2682
- Promise.resolve().then(function () { return require('./chunk-stripe-card-form.cjs.js'); }),
2683
- ]);
2684
- const apiClient = new APIClient({
2685
- orgId: config.orgId,
2686
- baseUrl: config.baseUrl || DEFAULTS.BASE_URL,
2687
- });
2688
- return mountStripeCardForm(element, session, { ...params, apiClient });
2689
- }
2690
- async function purchaseStripeWallet(params) {
2691
- const config = resolveConfig(params, 'purchaseStripeWallet');
2692
- const [session, { purchaseWallet }] = await Promise.all([
2693
- sessionService.createSession({
2694
- orgId: config.orgId,
2695
- baseUrl: config.baseUrl,
2696
- region: config.region,
2697
- priceId: params.priceId,
2698
- externalId: params.externalId,
2699
- email: params.email,
2700
- clientMetadata: params.clientMetadata,
2701
- countryCode: params.countryCode,
2702
- integration: 'stripe',
2703
- }),
2704
- Promise.resolve().then(function () { return require('./chunk-stripe-wallet.cjs.js'); }),
2705
- ]);
2706
- const apiClient = new APIClient({
2707
- orgId: config.orgId,
2708
- baseUrl: config.baseUrl || DEFAULTS.BASE_URL,
2709
- });
2710
- return purchaseWallet(session, { ...params, apiClient });
2711
- }
2712
- async function getAvailableStripeWallet(params) {
2713
- const config = resolveConfig(params, 'getAvailableStripeWallet');
2714
- const [session, { getAvailableWallet }] = await Promise.all([
2715
- sessionService.createSession({
2716
- orgId: config.orgId,
2717
- baseUrl: config.baseUrl,
2718
- region: config.region,
2719
- priceId: params.priceId,
2720
- externalId: params.externalId,
2721
- email: params.email,
2722
- clientMetadata: params.clientMetadata,
2723
- countryCode: params.countryCode,
2724
- integration: 'stripe',
2725
- }),
2726
- Promise.resolve().then(function () { return require('./chunk-stripe-wallet.cjs.js'); }),
2727
- ]);
2728
- const result = await getAvailableWallet(session);
2729
- if (result === 'APPLE_PAY')
2730
- return exports.PaymentMethod.APPLE_PAY;
2731
- if (result === 'GOOGLE_PAY')
2732
- return exports.PaymentMethod.GOOGLE_PAY;
2733
- return null;
2734
- }
2735
- async function getAvailableStripePaymentMethods(params) {
2736
- const wallet = await getAvailableStripeWallet(params);
2737
- return wallet
2738
- ? [exports.PaymentMethod.PAYMENT_CARD, wallet]
2739
- : [exports.PaymentMethod.PAYMENT_CARD];
2740
- }
2741
2654
 
2742
2655
  /**
2743
2656
  * @fileoverview Main entry point for @funnelfox/billing
@@ -2749,16 +2662,11 @@ const Billing = {
2749
2662
  initMethod: initMethod,
2750
2663
  silentPurchase: silentPurchase,
2751
2664
  getAvailablePaymentMethods: getAvailablePaymentMethods,
2752
- stripe: {
2753
- createCardForm: createStripeCardForm,
2754
- purchaseWallet: purchaseStripeWallet,
2755
- getAvailableWallet: getAvailableStripeWallet,
2756
- getAvailablePaymentMethods: getAvailableStripePaymentMethods,
2757
- },
2758
2665
  };
2759
2666
  if (typeof window !== 'undefined') {
2760
2667
  window.Billing = Billing;
2761
2668
  }
2669
+ console.debug('Billing SDK inited');
2762
2670
 
2763
2671
  exports.APIError = APIError;
2764
2672
  exports.Billing = Billing;
@@ -2778,4 +2686,3 @@ exports.configure = configure;
2778
2686
  exports.createCheckout = createCheckout;
2779
2687
  exports.createClientSession = createClientSession;
2780
2688
  exports.getAvailablePaymentMethods = getAvailablePaymentMethods;
2781
- exports.loadStripe = loadStripe;