@funnelfox/billing

中文文档
0.14.0发布于 2026/11/12MITTypeScriptESM + CJS构建来源已验证近 30 天下载 8428

JavaScript SDK for Funnelfox billing with Primer integration

原始使用文档
文档目录31 节
  1. Features
  2. Installation
  3. Via CDN
  4. Via NPM
  5. Quick Start
  6. API Reference
  7. configure(config)
  8. createCheckout(options)
  9. createClientSession(params)
  10. CheckoutInstance
  11. Complete Example
  12. Error Handling
  13. Common Error Codes
  14. TypeScript Support
  15. Advanced Usage
  16. Using Callbacks Instead of Events
  17. Custom Card Input Selectors
  18. Custom Payment Method Order
  19. Using initMethod for Single Payment Methods
  20. Manual Session Creation
  21. One-Click Upsell With Wallet Re-Authentication
  22. Billing.mountWalletUpsell(container, options)
  23. Stripe Integration
  24. Billing.stripe.createCardForm(element, params)
  25. Billing.stripe.getAvailableWallet(params)
  26. Billing.stripe.purchaseWallet(params)
  27. Billing.stripe.getAvailablePaymentMethods(params)
  28. Combined Example: Wallet Detection + Card Form Fallback
  29. Browser Support
  30. Examples
  31. License

@funnelfox/billing

A modern TypeScript SDK for subscription payments with Primer Headless Checkout integration.

Features

  • 🚀 Modern API: Clean, Promise-based interface with event-driven architecture
  • 🔄 Dynamic Pricing: Update prices without page reload
  • 🛡️ Type-Safe: Complete TypeScript definitions and type safety
  • 🎯 Event-Driven: Handle success, errors, and status changes with ease
  • 🔧 Robust: Built-in error handling, retries, and validation
  • 📦 Lightweight: Minimal dependencies, browser-optimized
  • 🎨 Headless Checkout: Full control over checkout UI with Primer Headless Checkout
  • 💳 Stripe Integration: Native Stripe Elements card form and Apple Pay / Google Pay wallets

Installation

Via CDN

<!-- Include Primer Headless Checkout SDK first -->
<script src="https://sdk.primer.io/web/v2.57.3/Primer.min.js"></script>
<link rel="stylesheet" href="https://sdk.primer.io/web/v2.57.3/Checkout.css" />

<!-- Include Funnelfox Billing SDK -->
<script src="https://unpkg.com/@funnelfox/billing@latest/dist/funnelfox-billing.min.js"></script>

Via NPM

npm install @funnelfox/billing @primer-io/checkout-web

If you are developing locally, install dev tooling for TypeScript builds/tests:

npm i -D @rollup/plugin-typescript ts-jest @types/jest

Then build:

npm run build

Quick Start

import { Billing } from '@funnelfox/billing';

await Billing.createCheckout({
  orgId: 'your-org-id',
  priceId: 'price_123',
  customer: {
    externalId: 'user_456',
    email: '[email protected]',
  },
  container: '#checkout-container',
});

API Reference

configure(config)

Configure global SDK settings.

import { configure } from '@funnelfox/billing';

configure({
  orgId: 'your-org-id', // Required
  baseUrl: 'https://custom.api', // Optional, defaults to https://billing.funnelfox.com
  region: 'us-east-1', // Optional, defaults to 'default'
});

Parameters:

  • config.orgId (string, required) - Your organization identifier
  • config.baseUrl (string, optional) - Custom API URL
  • config.region (string, optional) - Region, defaults to 'default'

createCheckout(options)

Creates a new checkout instance.

const checkout = await createCheckout({
  // Required
  orgId: 'your-org-id',
  priceId: 'price_123',
  customer: {
    externalId: 'user_456',
    email: '[email protected]', // Optional if you collect it in the card form
    countryCode: 'US', // Optional
  },
  container: '#checkout-container',
  clientMetadata: { source: 'web' },
  card: {
    emailAddress: {
      visible: true,
      template: '{{email}}',
    },
  },
  cardSelectors: {
    // Custom card input selectors (optional, defaults to auto-generated)
    cardNumber: '#cardNumberInput',
    expiryDate: '#expiryInput',
    cvv: '#cvvInput',
    cardholderName: '#cardHolderInput',
    emailAddress: '#emailAddressInput',
    button: '#submitButton',
  },
  paypalButtonContainer: '#paypalButton', // Optional
  googlePayButtonContainer: '#googlePayButton', // Optional
  applePayButtonContainer: '#applePayButton', // Optional
  paymentMethodOrder: ['PAYMENT_CARD', 'PAYPAL', 'GOOGLE_PAY', 'APPLE_PAY'], // Optional

  // Callbacks (alternative to events)
  onSuccess: result => {
    /* ... */
  },
  onError: error => {
    /* ... */
  },
  onStatusChange: (state, oldState) => {
    /* ... */
  },
});

Parameters:

  • options.priceId (string, required) - Price identifier
  • options.customer (object, required)
    • customer.externalId (string, required) - Your user identifier
    • customer.email (string, optional) - Customer email
    • customer.countryCode (string, optional) - ISO country code
  • options.container (string, required) - CSS selector for checkout container
  • options.card.emailAddress.visible (boolean, optional) - Shows an email field in the card form. Disabled by default.
  • options.card.emailAddress.template (string, optional) - Wraps the entered email before it is sent with payment, for example {{email}}.

Container Styling Requirements (Default Skin):

When using the default skin, the container element must have the following CSS properties for proper display of the loading indicator:

#checkout-container {
  position: relative;
  min-height: 200px; /* Adjust based on your layout */
}
  • position: relative - Required because the loading overlay uses position: absolute to cover the container
  • min-height - Required to ensure the loader is visible during initialization. Recommended minimum is 200px

Additional Parameters:

  • options.orgId (string, optional) - Org ID (if not configured globally)
  • options.clientMetadata (object, optional) - Custom metadata
  • options.cardSelectors (object, optional) - Custom card input selectors (defaults to auto-generated)
  • options.paypalButtonContainer (string, optional) - Container selector for PayPal button
  • options.googlePayButtonContainer (string, optional) - Container selector for Google Pay button
  • options.applePayButtonContainer (string, optional) - Container selector for Apple Pay button
  • options.paymentMethodOrder (array, optional) - Custom order for payment methods. Available values: 'PAYMENT_CARD', 'PAYPAL', 'GOOGLE_PAY', 'APPLE_PAY'. Defaults to ['PAYMENT_CARD', 'PAYPAL', 'GOOGLE_PAY', 'APPLE_PAY']
  • options.onInitialized (function, optional) - Initialized callback
  • options.onSuccess (function, optional) - Success callback
  • options.onError (function, optional) - Error callback
  • options.onStatusChange (function, optional) - State change callback

Returns: Promise<CheckoutInstance>


createClientSession(params)

Create a client session manually (for advanced integrations).

import { createClientSession } from '@funnelfox/billing';

const session = await createClientSession({
  priceId: 'price_123',
  externalId: 'user_456',
  email: '[email protected]', // Optional
  orgId: 'your-org-id', // Optional if configured
});

console.log(session.clientToken); // Use with Primer Headless Checkout
console.log(session.orderId);

Returns: Promise<{ clientToken: string, orderId: string, type: string }>


CheckoutInstance

Properties

  • id (string) - Unique checkout identifier
  • state (string) - Current state: initializing, ready, processing, completed, error
  • orderId (string) - Order identifier (available after initialization)
  • isDestroyed (boolean) - Whether checkout has been destroyed

Events

'success'

Emitted when payment completes successfully.

checkout.on('success', result => {
  console.log('Order ID:', result.orderId);
  console.log('Status:', result.status); // 'succeeded'
  console.log('Transaction:', result.transactionId);
});
'error'

Emitted when payment fails or encounters an error.

checkout.on('error', error => {
  console.error('Error:', error.message);
  console.error('Code:', error.code);
  console.error('Request ID:', error.requestId); // For support
});
'status-change'

Emitted when checkout state changes.

checkout.on('status-change', (newState, oldState) => {
  console.log(`${oldState} → ${newState}`);
  // States: initializing, ready, processing, action_required, completed, error
});
'destroy'

Emitted when checkout is destroyed.

checkout.on('destroy', () => {
  console.log('Checkout cleaned up');
});

Methods

updatePrice(priceId)

Updates the checkout to use a different price.

await checkout.updatePrice('price_yearly');

Note: Cannot update price while payment is processing.

getStatus()

Returns current checkout status.

const status = checkout.getStatus();
console.log(status.id); // Checkout ID
console.log(status.state); // Current state
console.log(status.orderId); // Order ID
console.log(status.priceId); // Current price ID
console.log(status.isDestroyed); // Cleanup status
destroy()

Destroys the checkout instance and cleans up resources.

await checkout.destroy();
isReady()

Check if checkout is ready for payment.

if (checkout.isReady()) {
  console.log('Ready to accept payment');
}
isProcessing()

Check if payment is being processed.

if (checkout.isProcessing()) {
  console.log('Payment in progress...');
}

Complete Example

<!DOCTYPE html>
<html>
  <head>
    <title>Funnelfox Checkout</title>
    <script src="https://sdk.primer.io/web/v2.57.3/Primer.min.js"></script>
    <link
      rel="stylesheet"
      href="https://sdk.primer.io/web/v2.57.3/Checkout.css"
    />
    <script src="https://unpkg.com/@funnelfox/billing@latest/dist/funnelfox-billing.min.js"></script>
  </head>
  <body>
    <div id="price-selector">
      <button onclick="selectPrice('price_monthly')">Monthly - $9.99</button>
      <button onclick="selectPrice('price_yearly')">Yearly - $99.99</button>
    </div>

    <div id="checkout-container"></div>

    <script>
      let currentCheckout = null;

      // Configure SDK once
      Billing.configure({
        orgId: 'your-org-id',
      });

      async function selectPrice(priceId) {
        try {
          if (currentCheckout && currentCheckout.isReady()) {
            // Update existing checkout
            await currentCheckout.updatePrice(priceId);
          } else {
            // Destroy old checkout if exists
            if (currentCheckout) {
              await currentCheckout.destroy();
            }

            // Create new checkout
            currentCheckout = await Billing.createCheckout({
              priceId: priceId,
              customer: {
                externalId: generateUserId(),
                email: getUserEmail(),
              },
              container: '#checkout-container',
            });

            // Handle success
            currentCheckout.on('success', result => {
              alert('Payment successful!');
              window.location.href = '/success?order=' + result.orderId;
            });

            // Handle errors
            currentCheckout.on('error', error => {
              alert('Payment failed: ' + error.message);
            });

            // Track state changes
            currentCheckout.on('status-change', state => {
              console.log('Checkout state:', state);
            });
          }
        } catch (error) {
          console.error('Checkout error:', error);
          alert('Failed to initialize checkout');
        }
      }

      function generateUserId() {
        return 'user_' + Math.random().toString(36).substr(2, 9);
      }

      function getUserEmail() {
        return '[email protected]'; // Get from your auth system
      }
    </script>
  </body>
</html>

Error Handling

The SDK provides specific error classes for different scenarios:

import {
  ValidationError,
  APIError,
  PrimerError,
  CheckoutError,
  NetworkError,
} from '@funnelfox/billing';

try {
  const checkout = await createCheckout(config);
} catch (error) {
  if (error instanceof ValidationError) {
    // Invalid input
    console.log('Field:', error.field);
    console.log('Value:', error.value);
    console.log('Message:', error.message);
  } else if (error instanceof APIError) {
    // API error
    console.log('Status:', error.statusCode);
    console.log('Error Code:', error.errorCode); // e.g., 'double_purchase'
    console.log('Error Type:', error.errorType); // e.g., 'api_exception'
    console.log('Request ID:', error.requestId); // For support
    console.log('Decline Code:', error.declineCode); // e.g., 'INSUFFICIENT_FUNDS', set on failed payments
    console.log('Decline Source:', error.declineCodeSource); // 'primer' | 'stripe' | 'adyen', when the provider reports one
    console.log('Message:', error.message);
  } else if (error instanceof PrimerError) {
    // Primer SDK error
    console.log('Primer error:', error.message);
    console.log('Original:', error.primerError);
  } else if (error instanceof CheckoutError) {
    // Checkout lifecycle error
    console.log('Phase:', error.phase);
    console.log('Message:', error.message);
  } else if (error instanceof NetworkError) {
    // Network/connectivity error
    console.log('Network error:', error.message);
    console.log('Original:', error.originalError);
  }
}

Common Error Codes

  • double_purchase - User already has an active subscription
  • invalid_price - Price ID not found
  • invalid_customer - Customer data validation failed
  • payment_failed - Payment processing failed

TypeScript Support

The SDK includes comprehensive TypeScript definitions:

import {
  configure,
  createCheckout,
  CheckoutInstance,
  PaymentResult,
  CheckoutConfig,
  PaymentMethod,
} from '@funnelfox/billing';

// Configure
configure({
  orgId: 'your-org-id',
});

// Create checkout with type safety
const checkout: CheckoutInstance = await createCheckout({
  priceId: 'price_123',
  customer: {
    externalId: 'user_456',
    email: '[email protected]',
    countryCode: 'US',
  },
  container: '#checkout',
  clientMetadata: {
    source: 'web',
    campaign: 'summer-sale',
  },
  paymentMethodOrder: [
    PaymentMethod.PAYPAL,
    PaymentMethod.PAYMENT_CARD,
    PaymentMethod.GOOGLE_PAY,
    PaymentMethod.APPLE_PAY,
  ],
});

// Type-safe event handlers
checkout.on('success', (result: PaymentResult) => {
  console.log('Order:', result.orderId);
  console.log('Status:', result.status);
  console.log('Transaction:', result.transactionId);
});

Advanced Usage

Using Callbacks Instead of Events

const checkout = await createCheckout({
  priceId: 'price_123',
  customer: {
    externalId: 'user_456',
    email: '[email protected]',
  },
  container: '#checkout',

  // Callback style (alternative to .on() events)
  onSuccess: result => {
    console.log('Success!', result.orderId);
  },
  onError: error => {
    console.error('Error!', error.message);
  },
  onStatusChange: (newState, oldState) => {
    console.log(`${oldState} → ${newState}`);
  },
});

Custom Card Input Selectors

By default, the SDK automatically generates card input elements. You can provide custom selectors if you want to use your own HTML structure:

const checkout = await createCheckout({
  priceId: 'price_123',
  customer: {
    externalId: 'user_456',
    email: '[email protected]',
  },
  container: '#checkout',

  // Custom card input selectors
  cardSelectors: {
    cardNumber: '#my-card-number',
    expiryDate: '#my-expiry',
    cvv: '#my-cvv',
    cardholderName: '#my-cardholder',
    emailAddress: '#my-email',
    button: '#my-submit-button',
  },

  // Custom payment method button containers
  paypalButtonContainer: '#my-paypal-button',
  googlePayButtonContainer: '#my-google-pay-button',
  applePayButtonContainer: '#my-apple-pay-button',
});

Custom Payment Method Order

You can customize the order in which payment methods are displayed to your customers:

const checkout = await createCheckout({
  priceId: 'price_123',
  customer: {
    externalId: 'user_456',
    email: '[email protected]',
  },
  container: '#checkout',

  // Customize payment method order
  paymentMethodOrder: ['PAYPAL', 'GOOGLE_PAY', 'APPLE_PAY', 'PAYMENT_CARD'],
});

Available payment methods:

  • 'PAYMENT_CARD' - Credit/debit card payment
  • 'PAYPAL' - PayPal payment
  • 'GOOGLE_PAY' - Google Pay payment
  • 'APPLE_PAY' - Apple Pay payment

By default, payment methods are shown in the order: Card, PayPal, Google Pay, Apple Pay. You can reorder them to match your business priorities or regional preferences.

Using initMethod for Single Payment Methods

For scenarios where you want to render a single payment method with full control over placement and callbacks:

import { Billing, PaymentMethod } from '@funnelfox/billing';

const container = document.getElementById('payment-container');

const paymentMethod = await Billing.initMethod(
  PaymentMethod.PAYMENT_CARD, // or PAYPAL, GOOGLE_PAY, APPLE_PAY
  container,
  {
    // Required
    orgId: 'your-org-id',
    priceId: 'price_123',
    externalId: 'user_456',
    email: '[email protected]',

    // Optional - API configuration
    baseUrl: 'https://custom.api', // Optional, defaults to https://billing.funnelfox.com
    meta: { source: 'web' }, // Optional metadata

    // Optional - Primer configuration (for customizing payment method behavior)
    style: {
      /* Primer style options */
    },
    card: {
      /* Primer card options */
    },
    applePay: {
      /* Primer Apple Pay options */
    },
    paypal: {
      /* Primer PayPal options */
    },
    googlePay: {
      /* Primer Google Pay options */
    },

    // Callbacks
    onRenderSuccess: () => {
      console.log('Payment method rendered successfully');
    },
    onRenderError: method => {
      console.error('Failed to render:', method);
    },
    onLoaderChange: isLoading => {
      console.log('Loading state:', isLoading);
    },
    onPaymentStarted: method => {
      console.log('Payment started with:', method);
    },
    onPaymentSuccess: () => {
      console.log('Payment completed successfully!');
    },
    onPaymentFail: error => {
      console.error('Payment failed:', error.message);
      console.log('Decline code:', error.declineCode); // e.g., 'INSUFFICIENT_FUNDS', when the provider reports one
      console.log('Decline source:', error.declineCodeSource); // 'primer' | 'stripe' | 'adyen'
    },
    onPaymentCancel: () => {
      console.log('Payment was cancelled');
    },
    onErrorMessageChange: message => {
      console.log('Error message:', message);
    },
    onMethodsAvailable: methods => {
      console.log('Available methods:', methods);
    },
  }
);

// Control the payment method
paymentMethod.setDisabled(true); // Disable the payment method
paymentMethod.setDisabled(false); // Enable it

// For card payments, you can trigger submit programmatically
if (paymentMethod.submit) {
  await paymentMethod.submit();
}

// Clean up when done
await paymentMethod.destroy();

Parameters:

  • method (PaymentMethod, required) - Payment method to initialize: PAYMENT_CARD, PAYPAL, GOOGLE_PAY, or APPLE_PAY
  • element (HTMLElement, required) - DOM element where the payment method will be rendered
  • options (InitMethodOptions, required):
    • orgId (string, required) - Your organization identifier
    • priceId (string, required) - Price identifier
    • externalId (string, required) - Your user identifier
    • email (string, optional) - Customer email
    • baseUrl (string, optional) - Custom API URL
    • meta (object, optional) - Custom metadata
    • style, card, applePay, paypal, googlePay (optional) - Primer SDK configuration options
    • Callbacks (all optional): onRenderSuccess, onRenderError, onLoaderChange, onPaymentStarted, onPaymentSuccess, onPaymentFail, onPaymentCancel, onErrorMessageChange, onMethodsAvailable

Returns: Promise<PaymentMethodInterface> with methods:

  • setDisabled(disabled: boolean) - Enable/disable the payment method
  • submit() - Trigger form submission (available for card payments)
  • destroy() - Clean up and remove the payment method

Manual Session Creation

For advanced integrations where you want to control the Primer Headless Checkout directly:

import { createClientSession } from '@funnelfox/billing';
import { Primer } from '@primer-io/checkout-web';

// Step 1: Create session
const session = await createClientSession({
  priceId: 'price_123',
  externalId: 'user_456',
  email: '[email protected]',
  orgId: 'your-org-id',
});

// Step 2: Use with Primer Headless Checkout directly
const headlessCheckout = await Primer.createHeadless(session.clientToken, {
  paymentHandling: 'MANUAL',
  apiVersion: '2.4',
  onTokenizeSuccess: async (paymentMethodTokenData, handler) => {
    // Your custom payment logic...
    // Call your payment API with paymentMethodTokenData.token
    handler.handleSuccess();
  },
});

await headlessCheckout.start();

One-Click Upsell With Wallet Re-Authentication

Billing.mountWalletUpsell(container, options)

A saved Apple Pay / Google Pay token must not be charged as a customer-initiated payment (Primer), so a buyer who paid with a wallet confirms the upsell in the wallet sheet again, and the upsell is charged with that fresh wallet payment. Call mountWalletUpsell when the upsell screen loads, not on tap: Apple Pay opens its sheet only from the tap itself, so the SDK prepares everything in advance and mounts Primer's native wallet button into container.

const mount = await Billing.mountWalletUpsell(container, {
  orgId: 'your-org-id',
  priceId: 'upsell_price',
  externalId: 'user_456',
  clientMetadata: { ff_billing_reason: 'upsell' },
  style: {
    buttonType: 'plain',
    buttonColor: 'black',
    height: 54,
    borderRadius: 28,
  },
  onStart: () => {},
  onThreeDSStart: ({ orderId }) => {},
  onThreeDSEnd: ({ result }) => {},
  onResult: result => {
    // result.outcome: 'succeeded' | 'declined' | '3ds_failed' | '3ds_abandoned'
    //   | 'error' | 'processing' | 'already_purchased' | 'cancelled'
  },
});

if (mount.mounted) {
  // The wallet button is shown: hide your own upsell button.
  // mount.setDisabled(true | false), mount.destroy() when the screen goes away.
} else if (mount.reason === 'not_wallet' || mount.reason === 'error') {
  // Keep your own button and use Billing.silentPurchase({ handle3DS: true }).
} else if (mount.reason === 'wallet_unavailable') {
  // The buyer's wallet cannot be shown on this device: open the full checkout on tap.
}

Mount result:

  • mounted: true with wallet (APPLE_PAY or GOOGLE_PAY): the native button is shown.
  • not_wallet: the buyer's saved method is not a Primer wallet.
  • wallet_unavailable: the device cannot show the buyer's wallet. The other wallet is never offered instead.
  • error with error: a request failed (network, 5xx, double_purchase, invalid_price, merchant_config_error, rate limit) or the button could not be rendered.
  • The promise rejects only with ValidationError. With mounted: false nothing is left in container.

onResult is called exactly once per attempt, with the same fields as silentPurchase({ handle3DS: true }) plus the outcome cancelled: the buyer closed the wallet sheet and nothing was charged. After cancelled the button stays active and can be tapped again; after any other outcome it is disabled for good. 3DS, retries with an idempotency key and the meaning of each outcome are those of silentPurchase({ handle3DS: true }); processing carries the order id the charge was reserved under.

Handled by the SDK:

  • A second tap while an attempt runs is ignored, for both wallets, and so is a silentPurchase({ handle3DS: true }) for the same buyer.
  • The wallet session is prepared again before it expires and when the tab becomes visible with little time left. A tap on a session about to expire is ignored and the button is replaced, so the next tap works.
  • When the backend reports the wallet session as no longer valid (nothing charged), the SDK prepares a new one and retries once with the same wallet payment, but only if the amount is unchanged. Otherwise, and when the wallet payment itself is rejected, the result is error.
  • If the session cannot be prepared again while the button is shown, onResult gets an error without a tap and the button is disabled.

Style: buttonType plain | buy | pay | continue (a type the wallet does not have becomes plain; Apple Pay continue is drawn with Apple's <apple-pay-button>), buttonColor black | white | white-outline (Google Pay shows white-outline as white), height and borderRadius in px (default 54 and 28).

Apple Pay needs the page's domain to be verified for Apple Pay in Primer, as for the checkout.

Stripe Integration

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.

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.


Billing.stripe.createCardForm(element, params)

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).

const element = document.getElementById('card-form');

const cardForm = await Billing.stripe.createCardForm(element, {
  // Required
  priceId: 'price_123',
  externalId: 'user_456',

  // Optional
  orgId: 'your-org-id',
  email: '[email protected]',
  countryCode: 'US',
  showWallets: false, // show Apple Pay / Google Pay inside the form
  appearance: {
    // Stripe Elements appearance API
    theme: 'stripe',
  },

  // Callbacks
  onRenderSuccess: () => {
    document.getElementById('pay-button').disabled = false;
  },
  onLoaderChange: loading => {
    document.getElementById('pay-button').disabled = loading;
  },
  onPaymentSuccess: (paymentMethod, orderId) => {
    window.location.href = '/success?order=' + orderId;
  },
  onPaymentFail: error => {
    console.error('Payment failed:', error.message);
  },
});

// Wire up your own submit button
document.getElementById('pay-button').addEventListener('click', async () => {
  await cardForm.submit();
});

Key parameters:

ParameterTypeDescription
priceIdstringPrice identifier
externalIdstringYour user identifier
emailstring?Customer email
orgIdstring?Org ID (if not globally configured)
showWalletsboolean?Show Apple Pay / Google Pay inside the Stripe form
appearanceobject?Stripe Elements Appearance API
clientMetadataobject?Custom metadata attached to the order

Returns: Promise<{ submit: () => Promise<void> }>


Billing.stripe.getAvailableWallet(params)

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.

const wallet = await Billing.stripe.getAvailableWallet({
  priceId: 'price_123',
  externalId: 'user_456',
});

if (wallet === 'APPLE_PAY') {
  document.getElementById('apple-pay-btn').style.display = 'block';
} else if (wallet === 'GOOGLE_PAY') {
  document.getElementById('google-pay-btn').style.display = 'block';
} else {
  // No wallet available — show card form only
}

Returns: Promise<'APPLE_PAY' | 'GOOGLE_PAY' | null>

Call Billing.stripe.getAvailableWallet(...) while rendering the page (it decides whether to show the wallet button anyway). Besides detecting availability it prewarms the payment sheet, so purchaseWallet(...) inside the click handler can open it immediately — Safari cancels Apple Pay sheets that open after long async work in the click handler. Pass the same priceId/externalId/email/countryCode to both calls: they share one checkout session, and mismatched params create a second session and skip the prewarmed sheet.


Billing.stripe.purchaseWallet(params)

Triggers the native Apple Pay or Google Pay payment sheet. Call this on button click after confirming a wallet is available via getAvailableWallet.

document.getElementById('wallet-btn').addEventListener('click', async () => {
  await Billing.stripe.purchaseWallet({
    priceId: 'price_123',
    externalId: 'user_456',
    totalLabel: 'Premium Plan', // Label shown in the payment sheet

    onPaymentSuccess: (paymentMethod, orderId) => {
      window.location.href = '/success?order=' + orderId;
    },
    onPaymentFail: error => {
      console.error('Wallet payment failed:', error.message);
    },
    onPaymentCancel: () => {
      console.log('User cancelled');
    },
    onLoaderChange: loading => {
      document.getElementById('wallet-btn').disabled = loading;
    },
  });
});

Key parameters:

ParameterTypeDescription
priceIdstringPrice identifier
externalIdstringYour user identifier
totalLabelstring?Label shown next to the amount in the payment sheet
emailstring?Customer email
clientMetadataobject?Custom metadata attached to the order

Billing.stripe.getAvailablePaymentMethods(params)

Returns all Stripe payment methods available for the current device. Always includes PAYMENT_CARD; also includes a wallet method if one is detected.

const methods = await Billing.stripe.getAvailablePaymentMethods({
  priceId: 'price_123',
  externalId: 'user_456',
});

// methods: ['PAYMENT_CARD', 'APPLE_PAY'] or ['PAYMENT_CARD'] etc.
console.log('Available:', methods);

Returns: Promise<PaymentMethod[]> — always contains PAYMENT_CARD, optionally APPLE_PAY or GOOGLE_PAY


Combined Example: Wallet Detection + Card Form Fallback

The recommended pattern — show a wallet button when available, always show the card form as fallback:

import { Billing } from '@funnelfox/billing';

Billing.configure({ orgId: 'your-org-id' });

const params = {
  priceId: 'price_123',
  externalId: 'user_456',
  email: '[email protected]',
};

async function initCheckout() {
  // 1. Check for wallet availability
  const wallet = await Billing.stripe.getAvailableWallet(params);

  if (wallet) {
    const walletBtn = document.getElementById('wallet-btn');
    walletBtn.textContent =
      wallet === 'APPLE_PAY' ? 'Pay with Apple Pay' : 'Pay with Google Pay';
    walletBtn.style.display = 'block';

    walletBtn.addEventListener('click', () => {
      Billing.stripe.purchaseWallet({
        ...params,
        totalLabel: 'Premium Plan',
        onPaymentSuccess: (_, orderId) => {
          window.location.href = '/success?order=' + orderId;
        },
        onPaymentFail: err => alert(err.message),
        onPaymentCancel: () => console.log('Cancelled'),
      });
    });
  }

  // 2. Always mount card form as fallback
  const cardForm = await Billing.stripe.createCardForm(
    document.getElementById('card-form'),
    {
      ...params,
      onPaymentSuccess: (_, orderId) => {
        window.location.href = '/success?order=' + orderId;
      },
      onPaymentFail: err => alert(err.message),
      onLoaderChange: loading => {
        document.getElementById('pay-btn').disabled = loading;
      },
    }
  );

  document.getElementById('pay-btn').addEventListener('click', () => {
    cardForm.submit();
  });
}

initCheckout();

Browser Support

  • Chrome 60+
  • Firefox 55+
  • Safari 12+
  • Edge 79+

Examples

See the examples directory for more complete examples:

License

MIT © Funnelfox

版本与发布记录

@funnelfox/billing 共发布 124 个版本,首次发布于 2025/10/01;过去 12 个月发布了 122 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.14.0latest2026/11/123 天前
0.8.0-ffb-805.562026/11/096 天前
0.8.0-ffb-837.552026/11/069 天前
0.13.22026/11/0213 天前
0.8.0-ffb-758.542026/10/3016 天前
0.8.0-ffb-778.532026/10/3016 天前

其他发布通道:beta → 0.9.0-beta.7,add-release-docs → 0.8.0-add-release-docs.8,ffb-467-adyen → 0.9.0-ffb-467-adyen.25,ffb-395 → 0.8.0-ffb-395.26,ffb-apple-pay-continue-button → 0.9.3-ffb-apple-pay-continue-button.33,feat-defer-session-on-interaction → 0.9.3-feat-defer-session-on-interaction.34。

关于 @funnelfox/billing 的常见问题

@funnelfox/billing 是什么?

JavaScript SDK for Funnelfox billing with Primer integration

如何安装 @funnelfox/billing?

在项目目录运行 npm install @funnelfox/billing,或使用 pnpm add / yarn add / bun add。

@funnelfox/billing 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@funnelfox/billing 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@funnelfox/billing 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=14.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@funnelfox/billing 可以商用吗?

@funnelfox/billing 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@funnelfox/billing 的下载量有多少?

根据 npm 官方下载统计,@funnelfox/billing 在 2026/10/16 – 2026/11/14 这 30 天内被下载 8,428 次,日均约 281 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@funnelfox/billing 还在维护吗?

最新版本 0.14.0 发布于 2026/11/12(3 天前);至今共发布 124 个版本,其中过去 12 个月发布了 122 个;首次发布于 2025/10/01。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。