@flopay/shared

v1.8.2

Published

Shared types, error classes, constants, and validation helpers used across all FloPay packages. This package contains no runtime logic beyond error factories and validation functions -- it is primarily a type/constant library.

Downloads

8,406

Readme

@flopay/shared

Shared types, error classes, constants, and validation helpers used across all FloPay packages. This package contains no runtime logic beyond error factories and validation functions -- it is primarily a type/constant library.

Installation

pnpm add @flopay/shared

Within the monorepo, packages depend on it via workspace:*.

Quick Start

Environment Configuration

import { configureFlopay } from '@flopay/shared';

// Call once at app startup — determines which billing API URL is used
configureFlopay({ environment: 'production' }); // or 'staging'

// Or set NEXT_PUBLIC_FLOPAY_ENV=production in your .env file (no code needed)

Resolve Billing API URL

import { resolveBillingApiUrl } from '@flopay/shared';

// Reads from: explicit param → env var → configureFlopay() → staging fallback
const url = resolveBillingApiUrl();

Other Imports

import {
  FloPayError,
  validationError,
  SDK_VERSION,
  DEFAULT_APPEARANCE,
  NIGHT_APPEARANCE,
  THEMES,
  CURRENCY_MAP,
  getCurrencyByCountry,
  isValidPublishableKey,
} from '@flopay/shared';

// Pick a coherent theme bundle and pass both halves to your checkout
const { appearance, buttonsLayout } = THEMES['glass-dark'];

import type {
  FloPayConfig,
  FloPayEnvironment,
  CheckoutSession,
  PaymentResult,
  PaymentProviderAdapter,
  FloPayAppearance,
} from '@flopay/shared';

// Validate a key
isValidPublishableKey('pk_test_abc123'); // true

// Look up currency by country
const info = getCurrencyByCountry('DE');
// { currency: 'EUR', symbol: '\u20ac', country: 'Germany', countryCode: 'DE', tax: 1 }

// Create a structured error
throw validationError('Email is required', 'email');

API Reference

Types

| Type | Description | |------|-------------| | FloPayConfig | Top-level config: publishableKey, locale?, appearance?, apiVersion?, telemetry? (set false to opt out of browser operational telemetry) | | FloPayAppearance | Theme config: theme ('default'/'flat'/'night'/'none'), variables?, rules? | | FloPayThemeVariables | CSS custom property overrides: colorPrimary, colorBackground, colorText, colorDanger, borderRadius, fontFamily, fontSizeBase, spacingUnit | | ButtonsLayoutStyles | Style overrides for the buttons layout: cardButton, cardButtonFontSize, cardFormContainer, cardInputBorder, cardInputColor, cardInputPlaceholderColor, cardInputFontSize, cardInputBackground, nameInput, backButton, backButtonFontSize, backButtonIcon, submitButton, submitButtonFontSize, title, titleFontSize, errorBanner | | ButtonsLayoutTheme | Theme preset name: 'default' / 'minimal' / 'rounded' / 'dark' / 'modern-light' / 'modern-dark' / 'bold-light' / 'bold-dark' / 'glass-light' / 'glass-dark' | | ThemeBundle | A coherent theme bundle: { appearance: FloPayAppearance, buttonsLayout: ButtonsLayoutStyles } | | ThemeBundleId | Bundle id: 'modern-light' / 'modern-dark' / 'bold-light' / 'bold-dark' / 'glass-light' / 'glass-dark' | | ThemeId | Accepted by the React components' theme prop: 'classic'ThemeBundleId | | CheckoutMode | Checkout mode: 'full' / 'auto' / 'confirm' | | CheckoutSessionMode | Mode accepted on session reads: CheckoutMode plus 'setup'. Browser session-creation types deliberately continue to reject 'setup'. | | CheckoutSession | Session object: id, clientSecret, mode, status (open/authorized/complete/expired), amount, currency, captureMethod?, paymentId?, authorizationExpiresAt?, failureOutcome?, customer?, checkoutMode?, vault? (hosted card widget), products? (unified CheckoutSessionProduct[]), successUrl?, cancelUrl?, coupons?, subtotalAmount? / discountAmount? / totalAmount? (cart-currency major units; populated by billing API ≥ v1.1.2), gateways? (map keyed by gateway code — stripe, paypal, …), accountData?, tagsData? | | SavedCardDisplay | Display-only saved-card DTO: FloPay id, brand, lastFour, expiryMonth, expiryYear, and lifecycle status. It contains no reusable credential. | | PaymentMethodRemovalError | Merchant-server removal union covering indistinguishable 404 payment_method_not_found, the three stable 409 conflict codes, and retry-safe 502 payment_method_deletion_failed guidance. | | CheckoutSessionProduct | Unified product from session response: uuid, checkoutSessionId, type ('item'/'subscription'), code?, name?, description?, quantity, totalAmount?, overrideAmount?, currency?, metadata? | | CheckoutProductType | 'item' / 'subscription' | | CheckoutProduct | Unified product input for session creation: type, code?, name?, quantity?, totalAmount?, overrideAmount?, currency?, metadata? | | CheckoutGateway | Per-gateway config from the billing API. publishableKey? identifies the keyed gateway, including the direct-PayPal client ID at gateways.paypal.publishableKey. paypalPublishableKey? is distinct and Stripe-only: it is the dedicated Stripe sub-account key used to render PayPal through Stripe Elements. Also includes environment? ('sandbox'/'live'), stripeClientSecret? (Stripe-only), providerObjectType? (direct PayPal: 'order'/'subscription'/'setup_token'), and enabledPaymentMethods? (Stripe-only). | | CheckoutGateways | Map keyed by gateway code (e.g. stripe, paypal). A session can advertise multiple concurrent gateways. | | GatewayEnvironment | 'sandbox' / 'live' — drives the PayPal JS SDK script and any other environment-scoped behavior | | PaymentResult | Payment outcome: status ('authorized'/'succeeded'/'processing'/'requires_action'/'failed'); authorized results carry paymentId?, sessionId?, and authorizationExpiresAt? | | CheckoutFailureOutcome | Stable authorization lifecycle failure: 'authorization_declined' / 'authentication_required' / 'authorization_expired' / 'capture_failed' | | PayPalPaymentResult | Stripe-hosted PayPal outcome: status, paymentIntentId?, paymentMethodId?, error? | | CreateSessionIntentRequest | Discriminated Stripe wallet/APM or direct-PayPal intent request. PayPal sends intentKind: 'payment' for Orders/subscriptions and 'setup' for a standalone setup token. Direct card is not representable. | | SessionIntent | Matching discriminated response. Stripe carries clientSecret; PayPal carries a server-derived providerObjectType ('order'/'subscription'/'setup_token'). Provider, payment-method category/type/id, intent kind, and provider object ID are kept as separate fields. | | PayPalProviderObjectType | 'order' / 'subscription' / 'setup_token' — the direct-PayPal continuation selected by the backend and verified by the SDK. | | SessionIntentDeclineRequest | Non-card provider decline classification. Runtime reporting accepts safe classification codes only and rejects messages or identifier-shaped values. | | PaymentProviderAdapter | Minimal provider interface for initialization, raw-provider access, and lifecycle management. Card collection uses CardCaptureAdapter; React owns live Stripe Elements directly. | | Customer | id, email, firstName?, lastName?, gender?, city?, state?, country?, zip? | | LineItem | price?, priceData?, quantity | | PriceData | currency, unitAmount, productData, recurring? | | RecurringInterval | interval ('month'/'year'), intervalCount? | | BillingProvider | 'recurly' / 'chargebee' / 'stripe' | | TokenizedBody | id?, type?, threeDSecureActionResultTokenId?, originalPaymentMethodId?, gateway? ('stripe' / 'paypal' / custom), paymentMethodType? (Stripe wire identifier — 'card', 'apple_pay', 'cashapp', 'klarna', … for gateway='stripe'; 'paypal' for gateway='paypal'), isPaypal? (deprecated — replaced by gateway + paymentMethodType; while populated for PayPal compatibility, the SDK emits the boolean true, never the string "true") | | NormalizedCheckoutSession | Provider-agnostic session: provider, mode, data, raw? | | CheckoutModeKind | 'tokenize' / 'redirect' | | CreateSessionParams | Full session creation params: billingApiUrl, checkoutBaseUrl, clientId, currency (required — SDK throws FloPayError({ type: 'validation_error', code: 'CurrencyRequired' }) before issuing the request when none can be resolved), captureMethod? ('automatic'/'manual'; omitted preserves immediate capture), products? (unified shape; takes precedence), items?, subscriptions? (legacy — folded into products[] before send), account, successUrl, cancelUrl, checkoutMode?, couponCodes?, tagsData?, redirectParams?, setCookie?, timeoutMs?, utmMetadata? | | CheckoutSessionResult | { status: 201; redirectUrl; nonce } / { status: 204 } / { status: number } (nonce is the session-bound checkout token returned by the billing API — forward it on every continuation call against the same session via x-checkout-session-token) | | CheckoutItem | providerItemId, providerItemName?, quantity?, totalAmount, overrideAmount?, currency? | | CheckoutSubscription | providerPlanId, providerPlanName?, quantity?, totalAmount, overrideAmount?, currency? | | CheckoutAccount | userId, firstName?, lastName?, email, country?, gender?, city?, state?, zip? | | ProcessPaymentParams | sessionId, nonce (required — session-bound checkout token, forwarded as x-checkout-session-token), tokenizedData?, accountData, chv? | | CreateCustomerParams | email, name?, metadata? | | UpdateCustomerParams | email?, name?, metadata? | | WebhookEvent | id, type, data, created | | CurrencyInfo | currency, symbol, country, countryCode, tax | | CountryOption | code, name, flag | | TagsData | googleContainerId?, sessionId?, testEventCode? |

Error Classes

| Export | Description | |--------|-------------| | FloPayError | Custom error class with type, code?, declineCode?, param?, statusCode? | | FloPayErrorType | 'validation_error' / 'api_error' / 'authentication_error' / 'rate_limit_error' / 'network_error' | | validationError(message, param?) | Factory for validation errors | | apiError(message, code?, statusCode?) | Factory for API errors | | authenticationError(message) | Factory for auth errors | | rateLimitError(message) | Factory for rate limit errors | | networkError(message) | Factory for network errors |

Privacy-safe telemetry contract

@flopay/shared exports the versioned closed telemetry types and builders used by both browser packages: buildTelemetryErrorEvent, buildTelemetryLogEvent, buildTelemetryPerformanceEvent, buildTelemetryTerminalEvent, and serializeTelemetryBatch. Builders copy only allowlisted enum/numeric/opaque fields; arbitrary messages, stacks, payloads, URLs, credentials, customer data, and unknown nested fields cannot enter serialized events.

Detached session creation (TeamFloPay/backend#1099) is measured as two stages so each phase can be tracked on its own: session_shell is create → mountable card form, and session_claim is the background attach of buyer and catalog data (request category session_claim, log names session.shell.ready, session.claim.started, session.claim.completed). session_create continues to span the whole logical create — shell and claim — so existing end-to-end dashboards keep measuring the same thing, and reports the create's attempt count rather than the claim's.

TELEMETRY_LOG_NAMES is the complete SDK-authored lifecycle catalog. The TELEMETRY_FAILURE_CATEGORIES export is the complete technical-failure catalog: server_error, transport_error, invalid_response, and provider_runtime. TelemetryFailureCategory exposes the corresponding union type. failureCategory is optional and accepted only by buildTelemetryErrorEvent; legacy technical events may omit it, and every non-error builder drops injected category fields.

The serializer emits the backend-owned v1 envelope with the fixed closed package identity (@flopay/shared, @flopay/js, or @flopay/react) and event classes. See docs/TELEMETRY.md for the privacy and operational policy.

Host-owned Sentry filtering

Hosts that already use Sentry can install the dependency-free predicate as their beforeSend callback:

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

Sentry.init({ beforeSend: dropThirdPartyOnlyError });

It returns null only when every exception value has a non-empty stack, no frame resolves to app code or an @flopay/* module, and at least one frame resolves to a browser-extension URL or Stripe's hosted dahlia/stripe.js controller. Other frames in a dropped stack may be anonymous or pathless. Mixed, unrelated-CDN, incomplete, anonymous-only, and genuine app errors are returned unchanged by identity. Frame provenance comes from paths and module names; Sentry's in_app value is deliberately ignored because host source-map rewriting can mark third-party frames as app-owned.

The helper is pure and structurally typed (SentryEventLike / SentryStackFrameLike); it does not add an @sentry/* dependency, inspect messages, or bundle Sentry configuration.

Constants

| Export | Description | |--------|-------------| | SDK_VERSION | Current SDK version ('1.4.9') | | DEFAULT_API_BASE_URL | 'https://api.flopay.io' | | DEFAULT_API_VERSION | '2024-01-01' | | DEFAULT_APPEARANCE | Default theme (primary #4A49FF, white background, Poppins font) | | FLAT_APPEARANCE | Flat theme (minimal borders, 4px radius) | | NIGHT_APPEARANCE | Dark theme (dark background #1A1A2E, light text) | | MODERN_LIGHT_APPEARANCE | Clean & airy light — Inter, FloPay-blue primary #1785E0, slate text, 14px radius, soft input shadows | | MODERN_DARK_APPEARANCE | Clean & airy dark — Inter, sky-blue primary #60A5FA, deep-blue surface #0B1220, 14px radius | | BOLD_LIGHT_APPEARANCE | Bold & vibrant light — Inter, saturated FloPay blue #1785E0, 2px borders, 600+ font weight, gradient-ready | | BOLD_DARK_APPEARANCE | Bold & vibrant dark — Inter, sky #60A5FA on deep-blue #0C2B4D, accented borders | | GLASS_LIGHT_APPEARANCE | Glass / elevated light — translucent inputs with backdrop blur, FloPay-blue primary #1785E0, 16px radius | | GLASS_DARK_APPEARANCE | Glass / elevated dark — translucent inputs with backdrop blur, #60A5FA on slate #0F172A | | BUTTONS_LAYOUT_DEFAULT / _MINIMAL / _ROUNDED / _DARK | Original buttons-layout style presets (back-compat) | | BUTTONS_LAYOUT_MODERN_LIGHT / _MODERN_DARK | Wrapper styles paired with the modern appearance bundles | | BUTTONS_LAYOUT_BOLD_LIGHT / _BOLD_DARK | Wrapper styles paired with the bold appearance bundles | | BUTTONS_LAYOUT_GLASS_LIGHT / _GLASS_DARK | Wrapper styles paired with the glass appearance bundles | | THEMES | Record<ThemeBundleId, ThemeBundle> — pick THEMES['glass-dark'] to get { appearance, buttonsLayout } | | resolveTheme(id) | Resolve a ThemeId ('classic'ThemeBundleId) to its ThemeBundle; 'classic' and missing ids return undefined | | resolveButtonsLayoutTheme(name) | Map a ButtonsLayoutTheme name to its ButtonsLayoutStyles preset | | PAYPAL_VAULTED_PAYMENT_METHOD_TYPE | paypal_vaulted — in-band payment-method channel that opts a session intent/process request into Flo-owned PayPal Order and setup-token continuations; legacy paypal keeps provider-managed semantics. | | SUPPORTED_CARD_BRANDS | ['visa', 'mastercard', 'mastercard_debit', 'amex', 'discover'] | | STRIPE_EXPRESS_METHODS | ['apple_pay', 'google_pay', 'paypal', 'link', 'amazon_pay', 'klarna'] — Stripe method type identifiers supported by ExpressCheckoutElement (everything else returned in gateways.stripe.enabledPaymentMethods falls into the PaymentElement accordion in the React SDK) | | partitionStripeMethods(enabledMethods, { excludePaypal? }) | Split the backend's enabled list into { expressMethods, paymentElementMethods }. Drops card; also drops paypal when excludePaypal: true. | | stripeExpressMethodToOptionKey(method) | Map Stripe wire identifiers (apple_pay, google_pay, amazon_pay) to the camelCase keys (applePay, googlePay, amazonPay) expected by ExpressCheckoutElement's paymentMethods options map. | | CURRENCY_MAP | Country code to CurrencyInfo mapping (EU, GB, US, CA, NZ, AU) | | DEFAULT_CURRENCY | USD fallback |

Validation Helpers

| Export | Description | |--------|-------------| | getCurrencyByCountry(countryCode) | Returns CurrencyInfo for a country code, falls back to USD | | isValidPublishableKey(key) | Returns true if the key matches pk_(test|live)_... | | isValidSecretKey(key) | Returns true if the key matches sk_(test|live)_... | | isCardSetupCheckoutSession(session) | Returns true only for a session read with checkoutMode: 'setup', zero amount, and no products. Use it before mounting a no-charge card-setup surface. |

Postal Code Helpers

Country-aware postcode format validation, a faithful mirror of the billing API's own validatoris-valid-postal-code.validator.ts (TeamFloPay/backend#895), same pinned validator@13.15.35 isPostalCode, same country→ISO-2 normalization, same fail-open rules — so the SDK never blocks a postcode the server accepts nor opens its submit gate on one the server's PATCH …/account will reject with a 400. @flopay/react's AVS block uses these to block a malformed postcode before the card is captured and to show the expected format inline.

| Export | Description | |--------|-------------| | getPostalCodeLabel(countryCode) | Country-appropriate field label ('ZIP Code', 'Postcode', 'Eircode', …); defaults to 'Postal Code' | | isPostalCodeSupported(country) | true when validator has a postcode pattern for the country. Normalization matches the backend: a 2-letter code is passed straight through (so 'UK', not a validator locale, → false); non-2-letter names resolve via the backend's COUNTRY_ALIASES ('United Kingdom'GB). Unresolvable / no-postcode countries return false | | isValidPostalCode(country, postalCode) | true when the (trimmed) postcode matches the country's format. Fails open in exactly the backend's three cases: blank/whitespace postcode, unresolvable country, or an unsupported validator locale. Callers distinguish "required" (empty) from "malformed" by checking emptiness + isPostalCodeSupported themselves — an empty value fails open here, as it does server-side | | getPostalCodeExample(country) | The example postcode the backend embeds in its 400 message (US 12345 or 12345-6789, GB SW1A 1AA, CA A1A 1A1, …), or undefined when there is none. Kept verbatim in sync with the backend's POSTAL_CODE_EXAMPLES so the SDK's inline hint and the server's rejection message never contradict | | getStateFromPostalCode(country, postalCode) | Derives a US state / CA province code from a postcode for AVS enrichment; null for other countries or a malformed code |

Idempotency Helpers

Primitives for the stable checkout-create Idempotency-Key (TeamFloPay/backend#972). @flopay/js and @flopay/react use these automatically; import them only if you build your own create flow.

| Export | Description | |--------|-------------| | IDEMPOTENCY_KEY_HEADER | The header name ('Idempotency-Key') the SDK sends on POST /v1/checkouts/sessions. Optional on the backend — absent means the legacy, non-idempotent path. | | MAX_IDEMPOTENCY_KEY_LENGTH | 255 — the documented max length a supplied key may have. | | IDEMPOTENCY_IN_PROGRESS_CODE | Backend code ('IdempotencyKeyInProgress') marking a retryable in-progress replay: retry with the same key. Distinct from a payload-conflict (409), which is non-retryable. | | generateIdempotencyKey() | Returns a cryptographically random, high-entropy key (crypto.randomUUID(), else 16 random bytes). Returns undefined when no secure RNG exists, so callers omit the header rather than emit a weak key. Never derived from request data. | | resolveIdempotencyKey(supplied?) | Returns a supplied key unchanged after validating it is non-empty and ≤ MAX_IDEMPOTENCY_KEY_LENGTH (throws FloPayError('validation_error') otherwise, never echoing the value), or a freshly generated key when none is supplied. |

import { isPostalCodeSupported, isValidPostalCode, getPostalCodeExample } from '@flopay/shared';

isPostalCodeSupported('US');               // true
isPostalCodeSupported('AE');               // false — UAE has no postcodes
isPostalCodeSupported('United Kingdom');   // true  — full name → GB
isPostalCodeSupported('UK');               // false — 2-letter passthrough (not a validator locale)
isValidPostalCode('GB', 'SW1A 1AA');       // true
isValidPostalCode('GB', '12345');          // false — US shape in GB
isValidPostalCode('AE', '');               // true — fail open, never blocks
isValidPostalCode('US', '');               // true — blank fails open (enforce "required" separately)
getPostalCodeExample('CA');                // 'A1A 1A1'

Runtime dependency. Unlike the rest of @flopay/shared, these helpers pull in validator (imported via the validator/lib/isPostalCode.js submodule so bundlers only include the one check). It installs transitively when you depend on @flopay/shared.