@funnelfox/billing 0.8.0-beta.4 → 0.8.0-ffb-395.5

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,6 +11,7 @@ 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
14
15
 
15
16
  ## Installation
16
17
 
@@ -711,6 +712,216 @@ const headlessCheckout = await Primer.createHeadless(session.clientToken, {
711
712
  await headlessCheckout.start();
712
713
  ```
713
714
 
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
+
714
925
  ## Browser Support
715
926
 
716
927
  - 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.8.0-beta.4';
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',
@@ -1128,7 +1128,7 @@ class APIClient {
1128
1128
  async createClientSession(params) {
1129
1129
  const payload = {
1130
1130
  region: params.region || 'default',
1131
- integration_type: 'primer',
1131
+ integration_type: params.integration ?? 'primer',
1132
1132
  pp_ident: params.priceId,
1133
1133
  external_id: params.externalId,
1134
1134
  email_address: params.email,
@@ -1264,6 +1264,45 @@ 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
+
1267
1306
  var loaderHtml = "<div class=\"ff-sdk-loader-container\">\n <div class=\"ff-sdk-loader\"></div>\n</div>\n";
1268
1307
 
1269
1308
  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 }";
@@ -1858,6 +1897,12 @@ class CheckoutInstance extends EventEmitter {
1858
1897
  this.cardEmailAddress = this.checkoutConfig.customer.email;
1859
1898
  this.shouldApplySessionCardholderNameConfig =
1860
1899
  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
+ });
1861
1906
  this._setupCallbackBridges();
1862
1907
  }
1863
1908
  _setupCallbackBridges() {
@@ -1899,64 +1944,35 @@ class CheckoutInstance extends EventEmitter {
1899
1944
  }
1900
1945
  }
1901
1946
  async createSession() {
1902
- this.apiClient = new APIClient({
1903
- baseUrl: this.baseUrl || DEFAULTS.BASE_URL,
1947
+ const response = await sessionService.createSession({
1904
1948
  orgId: this.orgId,
1905
- timeout: DEFAULTS.REQUEST_TIMEOUT,
1906
- retryAttempts: DEFAULTS.RETRY_ATTEMPTS,
1907
- });
1908
- const sessionParams = {
1949
+ baseUrl: this.baseUrl,
1909
1950
  priceId: this.checkoutConfig.priceId,
1910
1951
  externalId: this.checkoutConfig.customer.externalId,
1911
1952
  email: this.checkoutConfig.customer.email,
1912
- region: this.region || DEFAULTS.REGION,
1953
+ region: this.region,
1913
1954
  clientMetadata: this.checkoutConfig.clientMetadata,
1914
1955
  countryCode: this.checkoutConfig.customer.countryCode,
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;
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);
1960
1976
  }
1961
1977
  this.cachedSessionResponse = sessionResponse;
1962
1978
  this.isTelemetryEnabled = !!sessionResponse.data?.sdk_telemetry_enabled;
@@ -2215,8 +2231,7 @@ class CheckoutInstance extends EventEmitter {
2215
2231
  try {
2216
2232
  this.onLoaderChangeWithRace(true);
2217
2233
  this._setState('updating');
2218
- // Invalidate session cache
2219
- CheckoutInstance.sessionCache.clear();
2234
+ sessionService.clearCache();
2220
2235
  await this.apiClient.updateClientSession({
2221
2236
  orderId: this.orderId,
2222
2237
  clientToken: this.clientToken,
@@ -2249,7 +2264,7 @@ class CheckoutInstance extends EventEmitter {
2249
2264
  return;
2250
2265
  try {
2251
2266
  this.stopUnhandledTelemetry();
2252
- CheckoutInstance.sessionCache.clear();
2267
+ sessionService.clearCache();
2253
2268
  await this.primerWrapper.destroy();
2254
2269
  this._setState('destroyed');
2255
2270
  this.orderId = null;
@@ -2460,7 +2475,6 @@ class CheckoutInstance extends EventEmitter {
2460
2475
  this.telemetryPaymentMethod = undefined;
2461
2476
  }
2462
2477
  }
2463
- CheckoutInstance.sessionCache = new Map();
2464
2478
 
2465
2479
  /**
2466
2480
  * @fileoverview Public API with configuration and orchestration logic
@@ -2651,6 +2665,79 @@ async function getAvailablePaymentMethods(params) {
2651
2665
  throw error;
2652
2666
  }
2653
2667
  }
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
+ }
2654
2741
 
2655
2742
  /**
2656
2743
  * @fileoverview Main entry point for @funnelfox/billing
@@ -2662,6 +2749,12 @@ const Billing = {
2662
2749
  initMethod: initMethod,
2663
2750
  silentPurchase: silentPurchase,
2664
2751
  getAvailablePaymentMethods: getAvailablePaymentMethods,
2752
+ stripe: {
2753
+ createCardForm: createStripeCardForm,
2754
+ purchaseWallet: purchaseStripeWallet,
2755
+ getAvailableWallet: getAvailableStripeWallet,
2756
+ getAvailablePaymentMethods: getAvailableStripePaymentMethods,
2757
+ },
2665
2758
  };
2666
2759
  if (typeof window !== 'undefined') {
2667
2760
  window.Billing = Billing;
@@ -2685,3 +2778,4 @@ exports.configure = configure;
2685
2778
  exports.createCheckout = createCheckout;
2686
2779
  exports.createClientSession = createClientSession;
2687
2780
  exports.getAvailablePaymentMethods = getAvailablePaymentMethods;
2781
+ exports.loadStripe = loadStripe;