@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/sharedWithin 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 validator — is-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 invalidator(imported via thevalidator/lib/isPostalCode.jssubmodule so bundlers only include the one check). It installs transitively when you depend on@flopay/shared.