npm package: @superlogic/spree-pay
Description: Spree-pay React component and utilities
Latest version: 0.4.1

# @superlogic/spree-pay

Publishable React package exposing the SpreePay component and related utilities.

## Install

```sh
npm i @superlogic/spree-pay
```

## Usage

```tsx
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
        metadata: { orderId: order.id }, // Optional: additional data
      });

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

```typescript
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.

```tsx
<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

```typescript
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

```typescript
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)
  metadata?: Record<string, unknown>; // Optional: additional metadata to pass through
};

type PaymentMethodResult = {
  status: SlapiPaymentStatus; // 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

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

enum SlapiPaymentStatus {
  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)
}
```

### useCapture3DS Hook

```typescript
// Use in your 3DS redirect page to capture payment intent and close the modal
useCapture3DS(searchParams: Record<string, string | null>);
```

### Logger API

```typescript
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,
}
```

## Theme Customization

SpreePay uses CSS variables for theming (see all 58+ variables in `packages/spree-pay/src/styles/globals.css:22-82`). 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)

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

### Local/scoped override

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

```tsx
<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:

```tsx
<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)

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