npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@flopay/js

v1.6.0

Published

Browser-side FloPay SDK. Provides `loadFloPay()` to initialize the SDK, a `FloPay` class for managing payment elements and confirmations, `PaymentAPI` for billing API calls, and `createCheckoutSession` for server-initiated checkout flows.

Readme

@flopay/js

Browser-side FloPay SDK. Provides loadFloPay() to initialize the SDK, a FloPay class for managing payment elements and confirmations, PaymentAPI for billing API calls, and createCheckoutSession for server-initiated checkout flows.

Currently backed by Stripe via the StripeAdapter. The adapter pattern (PaymentProviderAdapter interface) allows swapping providers without changing consumer code.

Installation

pnpm add @flopay/js

@stripe/stripe-js ships as a direct dependency of @flopay/js — consumers do not need to install it separately.

Quick Start

Initialize the SDK

import { loadFloPay } from '@flopay/js';

const flopay = await loadFloPay('pk_test_...', {
  billingApiUrl: 'https://billing.example.com', // optional, enables retrieveSession/retrieveUnifiedSession
});

loadFloPay caches by publishable key and behavior-affecting options. Concurrent calls with the same key and options share one in-flight provider initialization and resolve to the same FloPay instance. A failed initialization is evicted so a later call can retry. The optional second argument accepts Omit<FloPayConfig, 'publishableKey'>, including billingApiUrl, locale, appearance, and telemetry; set telemetry: false to opt out of privacy-safe operational telemetry.

Create and Mount Non-card Elements

const elements = flopay.elements({
  amount: 2999,       // in cents
  currency: 'usd',
  paymentMethodTypes: ['cashapp', 'ideal'],
});

const paymentElement = await elements.create('payment');
paymentElement.mount(document.getElementById('payment-container')!);

paymentMethodTypes is required for payment elements and must contain at least one wallet/APM method. Any card entry is removed; a missing or empty non-card allowlist throws FloPayError('validation_error'). When Elements are initialized with a clientSecret, the provider intent is verified against the explicit non-card allowlist before mounting; the allowlist remains required and any extra provider method is rejected. Use @flopay/react's FloPayCheckout or SplitCardForm for card checkout; both mount the backend-hosted vault widget supplied in the session's vault block.

PayPal Payment

const result = await flopay.confirmPayPalPayment({
  billingApiUrl: 'https://billing.example.com',
  sessionId: 'session_uuid',
  nonce: sessionNonce,
  email: '[email protected]',
  returnUrl: window.location.href,
});

// After redirect return, resume the payment:
const resumed = await flopay.resumePayPalPayment();
if (resumed) {
  console.log('PayPal payment status:', resumed.status);
}

Direct-PayPal checkout surfaces use PaymentAPI.createSessionIntent() instead of confirmPayPalPayment(). The payment-method type is the channel selector: legacy paymentMethodType: 'paypal' (always intentKind: 'payment') returns an Order or provider-managed Subscription exactly as released, while paymentMethodType: 'paypal_vaulted' opts the request into the Flo-owned channel — intentKind: 'payment' returns a Flo-owned Order and intentKind: 'setup' returns a PayPal vault setup token. There is no capability header; sessions advertise availability via gateways.paypal.providerObjectType. See Flo-owned PayPal subscription continuations.

Retrieve a Session

// Using billingApiUrl from config (set at init time):
const session = await flopay.retrieveSession('session_uuid');
console.log(session.amount, session.currency, session.status);

// Or pass billingApiUrl explicitly:
const session2 = await flopay.retrieveSession('session_uuid', 'https://billing.example.com');

// For provider-specific data (Stripe clientSecret, publishableKey, etc.):
const unified = await flopay.retrieveUnifiedSession('session_uuid');
console.log(unified.data.stripe?.clientSecret);

Failed session loads reject with FloPayError. When the billing API returns a structured error body, the SDK preserves the safe message, backend code, and HTTP statusCode; body-less responses fall back to code: "http_<status>".

PaymentAPI (Billing API Client)

import { PaymentAPI } from '@flopay/js';

const api = new PaymentAPI('https://billing.example.com');

// Fetch and normalize a checkout session. `nonce` is the session-bound
// checkout token returned from session creation — required by post-#640
// backends, which match it against `checkout_session.nonce` before returning
// the row.
const session = await api.getUnifiedCheckoutSession('session_uuid', sessionNonce);
// session.provider === 'stripe' | 'chargebee' | 'recurly'
// session.data.session contains the normalized CheckoutSession
// session.data.session.clientSecret carries the same nonce so downstream
// code can re-use it.

// Create an Apple Pay intent. Direct-card requests are not part of this union.
const intent = await api.createSessionIntent(
  'session_uuid',
  sessionNonce,
  {
    provider: 'stripe',
    paymentMethodCategory: 'wallet',
    paymentMethodType: 'apple_pay',
    paymentMethodId: 'pm_xxx',
    intentKind: 'payment',
  },
);

// Report a client-observed non-card decline using a provider classification
// code only. Messages, identifiers, card data, credentials, and PII are rejected.
await api.reportSessionIntentDecline('session_uuid', sessionNonce, {
  provider: 'stripe',
  paymentMethodCategory: 'wallet',
  paymentMethodType: 'apple_pay',
  providerDeclineReason: 'payment_failed',
});

// Process a tokenized payment. `nonce` is required: the SDK throws a
// `FloPayError` with code `MissingCheckoutSessionToken` when it is missing,
// and otherwise POSTs to `/v1/checkouts/sessions/<id>/process` with the
// header set.
const processResponse = await api.processPayment('user_id', {
  sessionId: 'session_uuid',
  nonce: sessionNonce,
  tokenizedData: { id: 'pm_xxx', type: 'card' },
  accountData: { userId: 'user_id', email: '[email protected]', firstName: 'John', lastName: 'Doe' },
});
// If the backend returns `202 checkout_processing`, the SDK keeps polling the
// checkout session until it completes or times out, then resolves/rejects.

// Check for prior payments (saved card UX)
const payments = await api.getPaymentsByEmail('[email protected]');

createCheckoutSession

import { createCheckoutSession } from '@flopay/js';

const result = await createCheckoutSession({
  billingApiUrl: 'https://billing.example.com',
  checkoutBaseUrl: 'https://checkout.example.com',
  clientId: 'client_123',
  items: [{
    providerItemId: 'prod_abc',
    providerItemName: 'Pro Plan',
    totalAmount: 49.99,
    overrideAmount: 24.99,
  }],
  account: { userId: 'user_1', email: '[email protected]' },
  successUrl: '/success',
  cancelUrl: '/cancel',
  redirectParams: { email: '[email protected]', bg: 'courses', mode: 'confirm' },
});
// On 201: browser redirects to checkout page. The resolved
// `CheckoutSessionResult` is `{ status: 201, redirectUrl, nonce }` — `nonce` is
// the session-bound checkout token returned by the billing API. A matching
// `flopay_checkout_token` cookie is written to the same parent domain as
// `checkout_data`, so a hosted-checkout page can read the token from
// `document.cookie` and forward it on continuation calls without having to
// pull it from the URL (kept out of history/Referer).
// On 204: browser redirects to successUrl (payment method on file)

For an item-only card checkout that should place a hold instead of capturing immediately, add captureMethod: 'manual'. The same option is supported by createCheckoutSessionWithRetries, PaymentAPI.createAndFetchSession, and PaymentAPI.createDetachedSession. Omitting it keeps the request and result behaviour unchanged.

An accepted hold surfaces as status: 'authorized' on normalized sessions and payment results, with paymentId, sessionId, and authorizationExpiresAt. Treat it as checkout-terminal but not paid. Capture is a merchant-authenticated REST operation; this browser package contains no merchant credential or capture capability. See the repository's pre-authorisation guide.

Use createCheckoutSessionWithRetries for automatic retry with jittered exponential backoff on transient transport, timeout, and idempotency-in-progress errors. By default it makes at most two retries (three total attempts). A single 12-second total deadline (customizable with timeoutMs) covers the initial request, every retry, and all backoff; all attempts receive the same AbortSignal.

Idempotent checkout creation

When a secure RNG is available, every checkout-session create sends a stable Idempotency-Key header so a timeout or lost response cannot mint a second session. The behavior is fully additive — existing integrations need no changes:

  • Automatic (default): the SDK generates one cryptographically random, high-entropy key per logical createCheckoutSession call and reuses it for every transport retry of that same operation. Two independent calls get different keys, even when their request bodies are identical. The generated key is never derived from customer data and is never logged or returned. Automatic generation requires a secure RNG (crypto.randomUUID / crypto.getRandomValues); in an environment without one the SDK omits the header rather than emit a weak key — supply a valid idempotencyKey yourself to stay idempotent there.
  • Merchant-supplied: pass idempotencyKey to control the key yourself. It is sent unchanged and reused across retries, so you can keep it stable across React remounts, multiple SDK instances, or server retries you control. It identifies exactly one logical checkout creation and must not be reused for a new purchase. It must be non-empty and at most 255 characters (an invalid value throws a FloPayError('validation_error') before any request).
// Merchant-controlled key — reused for this one logical checkout only.
await createCheckoutSession({ /* … */ idempotencyKey: `checkout:${orderId}` });

createCheckoutSessionWithRetries resolves the key once, before its retry loop, so all attempts — timeouts, lost responses, and the backend's documented in-progress reply — replay the same key. A payload-conflict (409) is surfaced without retrying. The header is optional on the backend: older SDKs and direct API clients that omit it keep the legacy, unkeyed path. The same automatic and merchant-supplied behavior applies to inline session creation via PaymentAPI.createAndFetchSession (used by @flopay/react). Inline creation uses the same 12-second total deadline and jitter policy, with one shared three-attempt budget across network failures and idempotency-in-progress responses. Pass timeoutMs in the inline session params to choose a different UX ceiling.

@flopay/shared also exports the primitives directly if you build your own create flow: generateIdempotencyKey(), resolveIdempotencyKey(supplied?), IDEMPOTENCY_KEY_HEADER, and MAX_IDEMPOTENCY_KEY_LENGTH.

Detached session creation (default)

Inline session creation is detached by default (TeamFloPay/backend#1099). Instead of one request that resolves buyer identity and the catalog inline, the SDK issues two:

  1. POST /v1/checkouts/sessions with deferDataAttachment: true — a lightweight shell. It runs no catalog validation and takes none of the buyer-identity advisory locks that serialise concurrent checkouts for the same customer. The backend routes a gateway for it from currency and the buyer's country, so the response already carries gateways and the hosted vault block: the card form can mount from it.
  2. PATCH /v1/checkouts/sessions/{id}/claim — attaches buyer identity, address, products and coupons in the background, and returns the fully populated session.

PaymentAPI.createDetachedSession(params) exposes both phases:

const { shell, sessionId, nonce, claimed } = await api.createDetachedSession(params);

mountCardForm(shell.data.session?.vault);   // interactive immediately
const session = await claimed;              // cart, coupons, totals, buyer

Nothing may be charged before claimed settles: the billing API rejects process / intent / decline calls on an unclaimed session with 409 checkout_session_data_attachment_required, and holds an unclaimed vault charge with a retryable 503. @flopay/react gates the card widget's submit button for exactly this window.

createAndFetchSession uses the same two-phase flow but awaits the claim, so its resolved value is unchanged — existing callers get the reduced create contention with no code change. Detached creation requires checkoutMode: 'full', is skipped when tokenizedData is supplied, and there is no fallback to the one-shot create: it needs a billing API with the claim endpoint. Pass deferDataAttachment: false to force the one-shot create.

Because catalog validation moves to the claim, an invalid product or coupon surfaces as the same 422 — later in the flow, but with identical semantics. The claim body is fingerprinted server-side, so the SDK replays a byte-identical payload across transport retries; a materially different claim for an already-claimed session returns 409 and is surfaced without retrying.

Coupon validation errors are surfaced as FloPayError with structured code:

  • CouponLimitExceeded — more than 5 coupon codes supplied (also enforced client-side before the request leaves the browser).
  • CouponCurrencyUnsupported — an amount-based coupon has no price for the cart currency.
import { FloPayError } from '@flopay/shared';

try {
  await createCheckoutSession({ /* … */ couponCodes });
} catch (err) {
  if (err instanceof FloPayError) {
    if (err.code === 'CouponLimitExceeded') {
      // show "Too many coupons" toast
    } else if (err.code === 'CouponCurrencyUnsupported') {
      // show "Coupon is not valid for this currency" toast
    }
  }
}

API Reference

Privacy-safe operational telemetry

Browser telemetry is enabled by default and is best-effort. It reports only the closed SDK error/lifecycle/performance taxonomy through a Flo-owned API; it never bundles Sentry, reads Resource Timing, persists a queue, or transmits payment/customer/form data, credentials, URLs, provider payloads, arbitrary messages, or stacks. Existing merchant callbacks remain available with their existing arguments and invocation order. Callback exceptions and rejected promises are contained and logged only to the merchant console; they are not uploaded as SDK telemetry.

const flopay = await loadFloPay('pk_live_…', {
  telemetry: false, // merchant opt-out
});

const api = new PaymentAPI(billingApiUrl, { telemetry: false });
const vault = new PciVaultCardCapture({ telemetry: false });
await createCheckoutSession({ /* normal checkout fields… */ telemetry: false });

There is no custom telemetry endpoint or enrichment option. See docs/TELEMETRY.md for collected categories, exclusions, unauthenticated beacon transport, failure isolation, retention, and rollout gates.

The same default-on behavior and boolean opt-out apply to direct FloPay, PaymentAPI, createCheckoutSession, createCheckoutSessionWithRetries, and PciVaultCardCapture usage.

Genuine payment failures carry only a bounded technical category: server_error for backend 5xx failures, transport_error when no usable response arrives, invalid_response for malformed closed response shapes, and provider_runtime for provider SDK or hosted-widget runtime failures. Hosted vault widget errors use that category both before and after submission without including widget messages or session data. Categories are never derived from messages, response bodies, decline text, stacks, or identifiers. Backend payment declines and request validation failures use the non-error payment_declined and validation_rejected outcomes and carry no category. Uncategorized technical events remain serializable for backend rollout compatibility.

Hosts with their own Sentry client can filter recognized third-party-only browser errors without suppressing app-owned failures:

import * as Sentry from '@sentry/browser';
import { dropThirdPartyOnlyError } from '@flopay/js';

Sentry.init({ beforeSend: dropThirdPartyOnlyError });

This is a pure, dependency-free predicate rather than a bundled Sentry integration. It drops only complete exception stacks with no resolvable app or @flopay/* frame and at least one browser-extension URL or Stripe controller marker (dahlia/stripe.js path segment or dahlia/stripe module). Anonymous frames may accompany those markers. Mixed, unrelated, incomplete, and anonymous-only errors are returned unchanged. Detection ignores in_app, which can be rewritten to true for third-party frames by host tooling.

Exports

| Export | Description | |--------|-------------| | loadFloPay(publishableKey, options?) | Initializes the SDK. Returns a Promise<FloPay>. Shares successful and in-flight work by key; failed work remains retryable. | | FloPay | Main SDK class. Methods: elements(), submitElements(), cardCapture(), confirmPayment(), confirmPayPalPayment(), resumePayPalPayment(), retrieveSession(), retrieveUnifiedSession(), getRawProvider(), destroy() | | FloPayElements | Element group manager. Methods: create(type, options?), getElement(type), submit(), destroy() | | StripeAdapter | PaymentProviderAdapter implementation for Stripe | | PciVaultCardCapture | CardCaptureAdapter implementation that injects the backend-served hosted vault card widget. See Vault card capture. | | PaymentAPI | Billing API client. Includes session retrieval/creation, vault recovery, /process, createSessionIntent() (including PayPal payment/setup continuations), reportSessionIntentDecline(), completion polling, and saved-payment lookup. | | createCheckoutSession(options) | Creates a checkout session and redirects. Returns CheckoutSessionResult. | | createCheckoutSessionWithRetries(options) | Same as above with two retries by default (three total attempts) under one total deadline with jittered exponential backoff. | | dropThirdPartyOnlyError(event) | Host-owned Sentry beforeSend predicate. Drops recognized third-party-only exception stacks and otherwise returns the original event. | | toStripeAppearance(appearance) | Normalizes a FloPayAppearance into a StripeSafeAppearance by mapping FloPay's public theme token ('default' \| 'flat' \| 'night' \| 'none') onto Stripe's accepted set ('stripe' \| 'flat' \| 'night'). Use at any site that hands an appearance to @stripe/react-stripe-js's <Elements> so theme: 'default' never reaches Stripe.js (which would warn and silently fall back). variables/rules pass through untouched. StripeAdapter applies this normalization internally. | | toStripeAppearanceTheme(theme) | Lower-level helper backing toStripeAppearance: maps a single FloPay theme token to Stripe's. |

FloPay Class Methods

| Method | Returns | Description | |--------|---------|-------------| | elements(options?) | FloPayElements | Creates a new elements group. Destroys previous group. | | submitElements() | Promise<{ error? }> | Validates all mounted elements | | cardCapture(options?) | CardCaptureAdapter | Creates a hosted vault card-widget adapter (PciVaultCardCapture). See Vault card capture. | | confirmPayment(params) | Promise<PaymentResult> | Confirms a mounted wallet/APM using clientSecret, paymentMethodCategory, and paymentMethodType; rejects card. | | confirmPayPalPayment(params) | Promise<PayPalPaymentResult> | Stripe-hosted PayPal flow through the nonce-protected session intent contract, followed by confirmation/redirect. | | resumePayPalPayment() | Promise<PayPalPaymentResult \| null> | Resumes after PayPal redirect. Returns null if no PayPal params in URL. | | retrieveSession(sessionId, billingApiUrl?) | Promise<CheckoutSession> | Retrieves a checkout session by ID via GET /v1/checkouts/sessions/{id}. Uses billingApiUrl from config or the optional second argument. | | retrieveUnifiedSession(sessionId, billingApiUrl?) | Promise<NormalizedCheckoutSession> | Retrieves and normalizes a checkout session, including provider-specific data (Stripe clientSecret/publishableKey, etc.). | | getRawProvider() | unknown | Returns the raw underlying provider instance (e.g. Stripe object) | | destroy() | void | Tears down elements and provider |

Supported Element Types

  • payment -- Provider PaymentElement for supported non-card methods
  • address -- Address input element

Vault card capture

flopay.cardCapture() returns a CardCaptureAdapter that injects a backend-served, self-contained hosted vault widget (TeamFloPay/backend#823, Model A). The widget owns the PCI card fields, its own submit button, card tokenization, the PaymentIntent (created and confirmed server-side), 3DS, and the result — so no Stripe.js runs on the card path and PAN / CVC never enter the SDK runtime. The SDK's only job is to inject the widget HTML and relay its terminal postMessage outcome.

For a merchant-server-created no-charge setup session, @flopay/react uses the same adapter with operation: 'card_setup'. A valid terminal complete message may include paymentMethod, a reconstructed display-only object with FloPay id, brand, last four, expiry, and status. Unknown or malformed nested fields are dropped. During a mixed backend rollout the field may be absent; the result stays bound to the setup session and the SDK never invents an id. Setup terminal telemetry uses card_setup_succeeded / card_setup_declined, not payment-success outcomes.

// 1. Obtain the vault capture block — use an embedded block when supplied, or
//    fetch it through the explicit recovery route for a session whose Stripe
//    enabledPaymentMethods explicitly includes `card`. The first
//    argument is the checkout session id (`getVaultCapture(checkoutSessionId, nonce?)`):
const vault = session.vault ??
  await new PaymentAPI(billingApiUrl).getVaultCapture(checkoutSessionId, nonce);
const { html, messageToken, expectedOrigin } = vault;

// 2. Inject it and relay the widget's outcome.
const capture = flopay.cardCapture({
  sessionId: checkoutSessionId,
  captureMethod: session.captureMethod,
});
capture.on('complete', (e) => {
  if (e.outcome === 'authorized' || session.captureMethod === 'manual') {
    onComplete({
      status: 'authorized',
      paymentIntentId: e.intentId,
      paymentId: e.paymentId,
      sessionId: e.sessionId,
      authorizationExpiresAt: e.authorizationExpiresAt,
    });
    return;
  }
  onComplete({ status: 'succeeded', paymentIntentId: e.intentId });
});
capture.on('decline', (e) => onDecline(e.declineReason, e.message));
capture.on('error', (e) => onError(e.message));
// Pass the block's `messageToken` / `expectedOrigin` so the adapter can
// authenticate the widget's terminal postMessage (both optional).
await capture.mount(container, { html, messageToken, expectedOrigin });

Calling cardCapture() emits the separate vault.capture.requested lifecycle signal. The machine-duration vault_ready measurement starts when mount() begins and ends at the first validated widget ready message, so time before mount (including buyer time on a payment-method picker) is excluded. Duplicate ready messages do not emit additional readiness samples.

CardCaptureAdapter methods: mount(container, { html, messageToken?, expectedOrigin? }), on(event, handler) ('ready' | 'complete' | 'decline' | 'error'), unmount(). Outcome events arrive via a window.postMessage from the widget shaped { source: 'flopay-vault', type, sessionId, messageToken?, intentId?, declineReason?, message?, paymentMethod? }. The widget is injected same-window, so source alone is forgeable: the adapter rejects terminal complete / decline outcomes that are not bound to the mounted sessionId, that mismatch the mounted messageToken (when one was supplied), or that arrive from a non-matching expectedOrigin (when set).

Backend contract: TeamFloPay/backend#823. Starting with SDK 1.4.9, an explicit card entry in gateways.stripe.enabledPaymentMethods advertises that hosted capture can be recovered without an embedded vault block. PaymentAPI.getVaultCapture() coalesces concurrent callers, aborts a stalled attempt after ten seconds, and keeps bounded transient-network retry on the idempotent POST /v1/checkouts/sessions/{id}/vault/capture route.

Rollout boundary: continue embedding capture credentials for older/unknown SDKs and until the deployed 1.4.9+ adoption threshold is established. After that threshold, requests advertising x-flo-sdk-version: 1.4.9 or newer may omit the block during create/read/idempotent replay when card is advertised.