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

@superlogic/spree-pay

v0.5.7

Published

Spree-pay React component and utilities

Readme

@superlogic/spree-pay

Publishable React package exposing the SpreePay component and related utilities.

Install

npm i @superlogic/spree-pay

Usage

import { useState } from 'react';

import { SpreePay, SpreePayProvider, useSpreePay } from '@superlogic/spree-pay';
import { useCapture3DS } from '@superlogic/spree-pay';
import '@superlogic/spree-pay/styles.css';

const spreeEnv = {
  tenantId: 'your-tenant-id', // Tenant identifier provided during onboarding
  environment: 'dev' as const,
  redirect3dsURI: '/3ds', // Any valid URI in your app that captures 3ds redirect
  ssoPageURI: '/silent-check-sso.html', // Any valid URI in your app that will handle Keycloak SSO (example below)
};

function Checkout() {
  return (
    <SpreePayProvider env={spreeEnv}>
      <CheckoutContent />
    </SpreePayProvider>
  );
}

function CheckoutContent() {
  const { process, selectedPaymentMethod, isProcessing, enabled } = useSpreePay();
  const [status, setStatus] = useState<'idle' | 'processing' | 'success' | 'error'>('idle');

  async function handleProcess() {
    if (!enabled) {
      console.log('Please select a payment method');
      return;
    }

    setStatus('processing');
    try {
      // Example: create order and process payment
      const order = await getOrderHash();
      const result = await process({
        hash: order.hash,
        capture: true, // Optional: auto-capture payment
      });

      console.log('Payment result:', result);
      // result.status: AUTHORIZED or CAPTURED
      // result.paymentId: unique payment identifier
      // result.paymentType: CREDIT_CARD, CRYPTO, CDC, etc.

      setStatus('success');
    } catch (err) {
      // `process` always rejects with a PaymentError — see "Error Handling" below
      console.error('Payment failed:', err);
      setStatus('error');
    }
  }

  return (
    <div>
      <SpreePay
        amount={99}
        onProcess={handleProcess}
        transactionFeePercentage={0.04}
        className="w-full max-w-[540px]"
        isProcessing={status === 'processing'}
        disabledPoints={false}
        topUpLink="/top-up"
      />
      {status === 'success' && <p>Payment successful! Thank you for your order.</p>}
      {status === 'error' && <p>Payment failed. Please try again.</p>}
    </div>
  );
}

// example 3DS redirect page hosted on /3ds
export default function ThreeDSRedirectPage() {
  // take payment intent from search params as payment_intent
  const searchParams = exampleGetSearchParams();

  // Pass the entire search params object to the hook
  useCapture3DS({ paymentIntent: searchParams['payment_intent'] });

  // Render null or any other UI/Loader
  return null;
}

SpreePay props

type SpreePayProps = {
  amount?: number; // USD amount to charge (e.g., 99 for $99). Used for display and fee calculation.
  onProcess?: () => Promise<void>; // Called when the Checkout button is clicked. Implement your order flow here and call useSpreePay().process({ hash, ... }).
  isProcessing?: boolean; // Shows a loading state and disables interactions when true (useful while your onProcess runs).
  transactionFeePercentage?: number; // Fee multiplier applied to the USD portion (e.g., 0.04 for 4%).
  disabledPoints?: boolean; // Disables points payment option if true.
  topUpLink?: string; // Optional link to top-up page for points balance.
  className?: string; // Optional wrapper class for layout/styling.

  // Multi-currency display (all three fields should be supplied together)
  currencyCode?: string; // ISO 4217 display currency (e.g. "AUD").
  foreignCurrencyAmount?: number; // Pre-fee order total in the display currency (e.g. 1280.46). Used directly as Pay button base; transaction fee is added on top.
  exchangeRate?: number; // 1 unit of currencyCode in USD (e.g. 0.65 for AUD→USD). Used to convert USD remainder when points are applied.
  origin?: string; // Optional source identifier (e.g. ONE_PLATFORM)
};

Multi-currency display

When currencyCode, foreignCurrencyAmount, and exchangeRate are all provided the widget shows prices in the shopper-selected currency. Settlement and points calculations continue in USD.

<SpreePay
  amount={832.3} // USD amount — used for points math and payment-intent creation
  currencyCode="AUD"
  foreignCurrencyAmount={1280.46} // pre-fee order total in AUD from your backend
  exchangeRate={0.65} // 1 AUD = 0.65 USD
  onProcess={handleProcess}
  transactionFeePercentage={0.04}
  className="w-full max-w-[540px]"
  isProcessing={status === 'processing'}
/>

Display behaviour:

  • Pay button (no points) — shows foreignCurrencyAmount + fee in the display currency
  • Pay button (split) — shows the card portion converted from USD remainder + fee
  • Points slider — card amount updates live in the display currency as the slider moves
  • Available points value — shown in display currency alongside the points count

After the user commits the points slider, selectedPaymentMethod exposes:

  • amount — USD card amount including fee (use for payment-intent creation)
  • foreignCurrencyAmount — card amount in display currency, 2 d.p. (use for order display / receipts)

If any of the three display params are absent the widget falls back to USD-only display — no breaking change.

API Reference

SpreePayProvider Props

type ENV = {
  environment: 'dev' | 'stg' | 'prod'; // Environment for API endpoints and logging
  tenantId: string; // Tenant identifier
  ssoPageURI: string; // Path to Keycloak SSO page (e.g., '/silent-check-sso.html')
  redirect3dsURI: string; // Path to 3DS redirect handler (e.g., '/3ds')
  accessToken?: string; // Optional: provide pre-authenticated token to skip Keycloak SSO
  useWeb3Points?: boolean; // Optional: enable Web3-based points (default: true)
  keycloakClientId?: string; // Optional: custom Keycloak client ID
};

useSpreePay Hook

const {
  process, // Function to trigger payment processing
  isProcessing, // Internal processing state (true while payment is being processed)
  enabled, // true if a payment method is selected and ready
  selectedPaymentMethod, // Currently selected payment method details
} = useSpreePay();

// process function signature:
async function process(params: ProcessFnParams): Promise<PaymentMethodResult>;

type ProcessFnParams = {
  hash: string; // Order hash from your backend
  capture?: boolean; // Optional: auto-capture payment (default: false)
};

type PaymentMethodResult = {
  status: PaymentStatus; // Payment status (AUTHORIZED, CAPTURED, FAILED, etc.)
  paymentId: string; // Unique payment identifier
  txHash: string | null; // Transaction hash (for crypto payments)
  txId: string | null; // Transaction ID
  paymentType: PaymentType; // Type of payment used
};

Payment Types & Statuses

enum PaymentType {
  CREDIT_CARD = 'CREDIT_CARD',
  CRYPTO = 'CRYPTO',
  CDC = 'CDC', // Crypto.com Pay
  CREDIT_CARD_SPLIT = 'SPLIT', // Split payment (card + points)
  POINTS = 'POINTS',
}

enum PaymentStatus {
  AUTHORIZED = 'AUTHORIZED', // Payment authorized (success)
  CAPTURED = 'CAPTURED', // Payment captured (success)
  FAILED = 'FAILED', // Payment failed
  PENDING = 'PENDING', // Payment pending (backend only)
  NOT_INITIALIZED = 'NOT_INITIALIZED', // Not yet started (backend only)
  VOIDED = 'VOIDED', // Payment voided (backend only)
  CAPTURE_FAILED = 'CAPTURE_FAILED', // Capture failed (backend only)
}

Error Handling

process() always rejects with a PaymentError. Branch on code (the failure category) to show your own UI copy. The original error — including any raw backend detail — is kept on cause for logging only; never render it to end users.

| code | When | Suggested UX | | -------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | | PaymentErrorCode.CARD_ERROR | A card payment was rejected by the backend (a 4xx response, or a terminal FAILED status on a card flow). | "We couldn't process your card — check it / try another." | | PaymentErrorCode.PAYMENT_ERROR | Anything else: network/server error, timeout, cancelled flow, or a crypto/points failure. | Generic "payment failed, try again". |

Safety: message is a fixed, generic label (e.g. "Card payment failed") — it never contains raw backend text, so it's safe to log or show. To distinguish a real decline (e.g. "insufficient funds") from another 4xx, the backend must return a machine-readable error code; the widget intentionally does not surface the backend's free-text message to end users.

import { PaymentError, PaymentErrorCode, SpreePayHttpError, isPaymentError } from '@superlogic/spree-pay';

try {
  const result = await process({ hash, capture: true });
  // result.status is AUTHORIZED or CAPTURED
} catch (err) {
  if (isPaymentError(err)) {
    // Choose user-facing copy from the category — do NOT show err.cause to users.
    if (err.code === PaymentErrorCode.CARD_ERROR) {
      // card-payment problem — prompt the user to check the card or try another
    }

    err.code; // PaymentErrorCode — the category to branch on
    err.status; // PaymentStatus — e.g. FAILED
    err.backendCode; // string | undefined — a stable backend code when available (e.g. 'INSUFFICIENT_FUNDS')

    // For logging/debugging only — may carry sensitive backend detail:
    if (err.cause instanceof SpreePayHttpError) {
      err.cause.status; // numeric HTTP status (e.g. 400, 500)
      err.cause.body; // raw response body
      err.cause.url; // request URL
    }
  }
}
enum PaymentErrorCode {
  CARD_ERROR = 'CARD_ERROR', // a card payment was rejected by the backend
  PAYMENT_ERROR = 'PAYMENT_ERROR', // network/server/timeout/cancelled/crypto/points failure
}

// Thrown by the widget; detect it with `isPaymentError` and read its fields.
// Not meant to be constructed by consumers.
class PaymentError extends Error {
  message: string; // fixed, generic label — never raw backend text; safe to log/show
  code: PaymentErrorCode; // failure category to branch on (defaults to PAYMENT_ERROR)
  status: PaymentStatus; // payment status at the point of failure
  backendCode?: string; // stable machine code from the backend when available (e.g. 'INSUFFICIENT_FUNDS') — for finer branching
  cause?: unknown; // the original thrown value (standard Error.cause) — log only, may hold sensitive detail
}

// Type guard for consumers — the supported way to detect a PaymentError
function isPaymentError(e: unknown): e is PaymentError;

// Thrown by the fetcher on a non-ok HTTP response; reachable via PaymentError.cause
class SpreePayHttpError extends Error {
  status: number; // HTTP status code
  body: string; // raw response body
  url: string; // request URL
}

useCapture3DS Hook

// Use in your 3DS redirect page to capture payment intent and close the modal
useCapture3DS(params: { paymentIntent?: string | null });

Logger API

import { LogLevel, configureLogger, logger } from '@superlogic/spree-pay';

// Configure logging behavior
configureLogger({
  environment: 'dev', // 'prod' = ERROR only, 'dev'/'stg' = all logs
  minLevel: LogLevel.DEBUG, // Optional: override default log level
});

// Use the logger
logger.debug('Debug message', { context: 'data' });
logger.info('Info message');
logger.warn('Warning message');
logger.error('Error message', error);

// Create child logger with prefix
const componentLogger = logger.child('MyComponent');
componentLogger.info('Component initialized');

enum LogLevel {
  DEBUG = 0,
  INFO = 1,
  WARN = 2,
  ERROR = 3,
  NONE = 4,
}

Supporting types are exported so you can name them in your own code:

import type { Environment, LogContext, LoggerConfig } from '@superlogic/spree-pay';

type Environment = 'dev' | 'stg' | 'prod'; // also the `environment` field of the provider's ENV
type LoggerConfig = { minLevel: LogLevel; prefix: string; environment?: Environment };
type LogContext = Record<string, unknown>; // the optional context arg on logger.debug/info/warn/error

// configureLogger accepts a partial config:
function configureLogger(config: Partial<LoggerConfig> & { environment?: Environment }): void;

Theme Customization

SpreePay uses CSS variables for theming (the full list is the :root defaults block in packages/spree-pay/src/styles/globals.css). You can override these using CSS or CSS-in-JS.

Important: To override the theme locally (scoped), you need more specific CSS than just .sl-spreepay. Use a wrapper class like .your-class .sl-spreepay or combine with SpreePay's className prop.

Global override (affects all SpreePay instances)

.sl-spreepay {
  --primary: #272e32;
  --accent: #439ef4;
  --s-default: #ffffff;
}

Local/scoped override

Option A: Wrap SpreePay in a container with your own class:

<div className="my-checkout">
  <SpreePay {...props} />
</div>

<style>{`
  .my-checkout .sl-spreepay {
    --primary: #your-color;
    --accent: #your-accent;
  }
`}</style>

Option B: Pass className to SpreePay and use combined selector:

<SpreePay className="custom-theme" {...props} />

<style>{`
  .sl-spreepay.custom-theme {
    --primary: #your-color;
    --accent: #your-accent;
  }
`}</style>

Keycloak SSO page example (/silent-check-sso.html)

<!doctype html>
<html>
  <body>
    <script>
      try {
        parent.postMessage(location.href, location.origin);
      } catch (e) {}
    </script>
  </body>
</html>