npm package: @mallient/react
Description: React component library for identity and business verification — KYC and KYB flows with WebSocket-driven state, cross-device QR handoff, and theme support.
Latest version: 0.0.1-alpha.3

# @mallient/react

React component library for identity and business verification — KYC and KYB flows with WebSocket-driven state, cross-device QR handoff, and theme support.

---

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Import Paths](#import-paths)
- [MallientProvider](#mallientprovider)
  - [Props](#mallientprovider-props)
  - [Self-managed session](#self-managed-session)
  - [Pre-created session](#pre-created-session)
- [KYC — Identity Verification](#kyc--identity-verification)
  - [MallientKYC](#mallientkyc)
  - [How the session flow works](#how-the-session-flow-works)
  - [Props](#mallientkyc-props)
  - [Theme](#theming)
  - [Individual KYC components](#individual-kyc-components)
- [KYB — Business Verification](#kyb--business-verification)
  - [MallientKYB](#mallientkyb)
  - [Props](#mallientkyb-props)
  - [Individual KYB components](#individual-kyb-components)
- [Core utilities](#core-utilities)
- [TypeScript types](#typescript-types)
- [Development](#development)
- [Peer requirements](#peer-requirements)

---

## Installation

```bash
npm install @mallient/react
# or
yarn add @mallient/react
# or
pnpm add @mallient/react
```

React 18 or 19 is required as a peer dependency.

---

## Quick Start

```tsx
import { MallientProvider, MallientKYC } from '@mallient/react'

function VerifyPage() {
  return (
    <MallientProvider clientId="clt_live_abc123" domain="acme.com">
      <MallientKYC
        onComplete={() => router.push('/dashboard')}
        onFailed={(reason) => setError(reason)}
      />
    </MallientProvider>
  )
}
```

```tsx
// Business verification
import { MallientProvider } from '@mallient/react'
import { MallientKYB } from '@mallient/react/kyb'

function OnboardingPage() {
  return (
    <MallientProvider clientId="clt_live_abc123" domain="acme.com">
      <MallientKYB
        onSearchCompany={async (q) => fetchCompanies(q)}
        onUploadDocument={async (file) => uploadDoc(file)}
        onRequestKYCToken={async (uboId) => getKYCSession(uboId)}
        onSubmit={async () => submitApplication()}
      />
    </MallientProvider>
  )
}
```

---

## Import Paths

The package ships separate entry points for tree-shaking. Import only what you use:

| Path | Contents |
|------|----------|
| `@mallient/react` | `MallientProvider`, `MallientKYC`, `MallientKYB`, and shared types |
| `@mallient/react/kyc` | All KYC components + types |
| `@mallient/react/kyb` | All KYB components + types |
| `@mallient/react/core` | Core primitives: `MallientProvider`, `SessionProvider`, `StatusBadge`, `UploadZone`, `ErrorBoundary`, all types |
| `@mallient/react/kyc-widget` | Legacy form-based KYC widget |
| `@mallient/react/button` | Base `Button` component |

---

## MallientProvider

`MallientProvider` is the **single root context provider** for all Mallient widgets. Place it once near the top of your component tree. All `MallientKYC`, `MallientKYB`, `KYCElement`, and `useKYC` calls must be descendants.

### MallientProvider props

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `clientId` | `string` | ✓ (self-managed session) | Your Mallient client identifier. Required when the provider creates sessions automatically. Omit only when supplying a pre-created `sessionToken`. |
| `domain` | `string` | | Domain sent to the session service. Defaults to `window.location.hostname`. |
| `mobileVerifyBaseUrl` | `string` | | Base URL embedded in the QR code. Defaults to the current page's `origin + pathname`. |
| `sessionToken` | `string` | | Pre-created session token — bypasses automatic session creation. Use this for externally-managed sessions (e.g. KYB UBO flow). |
| `mobileUrl` | `string` | | Pre-built mobile handoff URL. Used together with `sessionToken`. |

### Self-managed session

The provider connects to the Mallient session service automatically using your `clientId` and `domain`. This is the default mode.

```tsx
import { MallientProvider, MallientKYC } from '@mallient/react'

export default function VerifyPage() {
  return (
    <MallientProvider clientId="clt_live_abc123" domain="acme.com">
      <MallientKYC
        onComplete={() => router.push('/done')}
        onFailed={(reason) => console.error(reason)}
      />
    </MallientProvider>
  )
}
```

### Pre-created session

When your backend provisions the session, pass `sessionToken` and `mobileUrl` to skip automatic session creation.

```tsx
import { MallientProvider, MallientKYC } from '@mallient/react'

export default function VerifyPage() {
  const { sessionToken, mobileUrl } = useVerificationSession() // your hook

  return (
    <MallientProvider sessionToken={sessionToken} mobileUrl={mobileUrl}>
      <MallientKYC
        onComplete={() => router.push('/done')}
        onFailed={(reason) => console.error(reason)}
      />
    </MallientProvider>
  )
}
```

---

## KYC — Identity Verification

### MallientKYC

The identity verification widget. Renders a styled card shell around the KYC flow.

Requires `<MallientProvider>` above it in the tree to supply the session context.

The widget handles:

- Live status updates via WebSocket
- Cross-device QR handoff (desktop shows QR → mobile captures)
- Final result display with pass/fail per check

#### Basic usage

```tsx
import { MallientProvider, MallientKYC } from '@mallient/react'

export default function VerifyPage() {
  return (
    <MallientProvider clientId="clt_live_abc123" domain="acme.com">
      <MallientKYC
        onComplete={() => {
          router.push('/dashboard')
        }}
        onFailed={(reason) => {
          console.error('Verification failed:', reason)
        }}
      />
    </MallientProvider>
  )
}
```

#### How the session flow works

```
MallientProvider (creates/connects session)
        │
        ▼
<MallientKYC>
        │
        ├─ WebSocket connects → status: initializing → awaiting_mobile
        │
        ├─ User scans QR / opens mobileUrl on phone
        │     └─ status: mobile_connected
        │
        ├─ Capture runs on device
        │     └─ status: in_progress
        │
        └─ Backend sends result via WebSocket
              ├─ verification.complete → status: complete → onComplete()
              ├─ verification.failed  → status: failed   → onFailed(reason)
              └─ review.escalated     → status: manual_review
```

#### MallientKYC props

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `onComplete` | `() => void` | | Called when verification succeeds |
| `onFailed` | `(reason: string) => void` | | Called when verification fails or session expires |
| `theme` | `KYCTheme` | | Visual theme overrides (see [Theming](#theming)) |
| `title` | `string` | | Widget header title (default: `'Identity Verification'`) |
| `loadingContent` | `ReactNode` | | Custom content shown while the session initializes. Defaults to a spinner. |

### Theming

All components are styled with CSS custom properties. Pass a `theme` object to override them on a per-instance basis:

```tsx
<MallientProvider clientId="clt_live_abc123" domain="acme.com">
  <MallientKYC
    theme={{
      primaryColor:     '#6366f1',          // --ml-primary
      borderRadius:     '12px',             // --ml-radius
      fontFamily:       'Inter, sans-serif', // --ml-font
      backgroundColor:  '#ffffff',          // --ml-bg
      textColor:        '#111827',          // --ml-text
    }}
    onComplete={() => router.push('/done')}
  />
</MallientProvider>
```

For global defaults across all components, set the custom properties in your own CSS:

```css
:root {
  --ml-primary: #6366f1;
  --ml-radius:  10px;
  --ml-font:    'Inter', sans-serif;
}
```

### Individual KYC components

Import from `@mallient/react/kyc` to compose your own layout. All components must be inside `<MallientProvider>`.

```tsx
import {
  KYCElement,
  PendingState,
  SuccessState,
  FailedState,
  ManualReviewState,
  QRDisplay,
  StatusBadge,
  CrossDeviceHandoff,
  VerificationStatus,
  ResultSummary,
  SessionStatus,
} from '@mallient/react/kyc'
```

#### KYCElement

The unstyled KYC flow — use this with your own layout instead of `MallientKYC`:

```tsx
import { MallientProvider } from '@mallient/react'
import { KYCElement } from '@mallient/react/kyc'

<MallientProvider clientId="clt_live_abc123" domain="acme.com">
  <div className="my-card">
    <KYCElement
      onComplete={() => router.push('/done')}
      onFailed={(reason) => setError(reason)}
    />
  </div>
</MallientProvider>
```

#### StatusBadge

Displays a colour-coded badge for any session status:

```tsx
import { StatusBadge } from '@mallient/react/core'

<StatusBadge status="in_progress" />
<StatusBadge status="complete" />
```

#### QRDisplay

Renders the mobile handoff QR code using an HTML5 canvas:

```tsx
import { QRDisplay } from '@mallient/react/kyc'

<QRDisplay
  url="https://verify.mallient.com/mobile/abc123"
  size={200}                          // optional, pixels (default: 200)
  hint="Scan with your phone"         // optional caption
/>
```
        │     └─ status: mobile_connected
        │
        ├─ Capture runs on device
        │     └─ status: in_progress
        │
        └─ Backend sends result via WebSocket
              ├─ verification.complete → status: complete → onComplete(result)
              ├─ verification.failed  → status: failed   → onFailed(reason)
              └─ review.escalated     → status: manual_review
```

#### MallientKYC props

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `sessionToken` | `string` | ✓ | Session token from your backend |
| `mobileUrl` | `string` | ✓ | Mobile capture deep link from your backend |
| `onComplete` | `(result: VerificationResult) => void` | | Called when verification succeeds |
| `onFailed` | `(reason: string) => void` | | Called when verification fails or session expires |
| `theme` | `KYCTheme` | | Visual theme overrides (see [Theming](#theming)) |
| `wsUrl` | `string` | | Override the WebSocket server URL (default: `wss://verify.mallient.com/ws`) |
| `title` | `string` | | Widget header title (default: `'Identity Verification'`) |

### Theming

All components are styled with CSS custom properties. Pass a `theme` object to override them on a per-instance basis:

```tsx
<MallientKYC
  sessionToken={token}
  mobileUrl={url}
  theme={{
    primaryColor:     '#6366f1',   // --ml-primary
    borderRadius:     '12px',      // --ml-radius
    fontFamily:       'Inter, sans-serif', // --ml-font
    backgroundColor:  '#ffffff',   // --ml-bg
    textColor:        '#111827',   // --ml-text
  }}
/>
```

For global defaults across all components, set the custom properties in your own CSS:

```css
:root {
  --ml-primary: #6366f1;
  --ml-radius:  10px;
  --ml-font:    'Inter', sans-serif;
}
```

### Individual KYC components

Import from `@mallient/react/kyc` to compose your own layout:

```tsx
import {
  PendingState,
  SuccessState,
  FailedState,
  ManualReviewState,
  QRDisplay,
  StatusBadge,
  CrossDeviceHandoff,
  VerificationStatus,
  ResultSummary,
  SessionStatus,
} from '@mallient/react/kyc'
```

#### SessionProvider

Wrap any subtree that needs access to the live WebSocket session state:

```tsx
import { SessionProvider, useSession } from '@mallient/react/core'

function MyFlow({ token, mobileUrl }: { token: string; mobileUrl: string }) {
  return (
    <SessionProvider token={token}>
      <MyCustomUI mobileUrl={mobileUrl} />
    </SessionProvider>
  )
}

function MyCustomUI({ mobileUrl }: { mobileUrl: string }) {
  const { state } = useSession()
  // state.status: 'idle' | 'initializing' | 'awaiting_mobile' | 'mobile_connected'
  //               | 'in_progress' | 'manual_review' | 'complete' | 'failed' | 'expired'
  return <p>Status: {state.status}</p>
}
```

#### StatusBadge

Displays a colour-coded badge for any session status:

```tsx
import { StatusBadge } from '@mallient/react/core'

<StatusBadge status="in_progress" />
<StatusBadge status="complete" />
```

#### QRDisplay

Renders the mobile handoff QR code using an HTML5 canvas:

```tsx
import { QRDisplay } from '@mallient/react/kyc'

<QRDisplay
  url="https://verify.mallient.com/mobile/abc123"
  size={200}                          // optional, pixels (default: 200)
  hint="Scan with your phone"         // optional caption
/>
```

---

---

## KYB — Business Verification

### MallientKYB

A 6-step wizard for business onboarding. Requires `<MallientProvider>` above it in the tree.

Steps:

1. **Company search** — look up by name or registration number (or enter manually)
2. **Business details** — legal name, registration number, jurisdiction, address, business type
3. **Document upload** — drag-and-drop articles of incorporation, licences, etc.
4. **Beneficial owners** — add UBOs (≥ 25% ownership) and trigger KYC per person
5. **Review** — summary before submission
6. **Submitted** — confirmation screen

#### Basic usage

```tsx
import { MallientProvider } from '@mallient/react'
import { MallientKYB } from '@mallient/react/kyb'
import type { CompanySearchResult, UploadedDocument } from '@mallient/react/core'

export default function BusinessOnboarding() {
  return (
    <MallientProvider clientId="clt_live_abc123" domain="acme.com">
      <MallientKYB
        onSearchCompany={searchCompanies}
        onUploadDocument={uploadDocument}
        onRequestKYCToken={getUBOKYCSession}
        onSubmit={submitApplication}
      />
    </MallientProvider>
  )
}

// Search your company registry API
async function searchCompanies(query: string): Promise<CompanySearchResult[]> {
  const res = await fetch(`/api/companies?q=${encodeURIComponent(query)}`)
  return res.json()
}

// Upload a document and return metadata
async function uploadDocument(file: File): Promise<UploadedDocument> {
  const form = new FormData()
  form.append('file', file)
  const res = await fetch('/api/documents', { method: 'POST', body: form })
  return res.json()
  // Must return: { id, name, type, size, uploadedAt, url? }
}

// Provision a KYC session for a beneficial owner
async function getUBOKYCSession(uboId: string) {
  const res = await fetch('/api/ubo-kyc', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ uboId }),
  })
  return res.json()
  // Must return: { sessionToken: string, mobileUrl: string }
}

// Final submission
async function submitApplication() {
  await fetch('/api/kyb/submit', { method: 'POST' })
}
```

#### MallientKYB props

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `onSearchCompany` | `(query: string) => Promise<CompanySearchResult[]>` | | Registry search. If omitted, users can only enter a name manually. |
| `onUploadDocument` | `(file: File) => Promise<UploadedDocument>` | | Called for each dropped file. If omitted, the upload zone is disabled. |
| `onRequestKYCToken` | `(uboId: string) => Promise<{ sessionToken: string; mobileUrl: string }>` | | Called when a UBO is added. If omitted, UBOs are listed but KYC is not triggered. |
| `onSubmit` | `() => Promise<void>` | | Called when the user clicks Submit. |
| `theme` | `KYCTheme` | | Same theme object as `MallientKYC`. |
| `title` | `string` | | Wizard header title (default: `'Business Verification'`). |

#### Company step — manual entry

If a company is not found in the registry (or no `onSearchCompany` is provided), users can type a name and click **Use this name** to continue. The wizard advances with `status: 'Unverified'` and the typed name pre-filled in the business details form.

### Individual KYB components

Import from `@mallient/react/kyb` to compose your own step layout:

```tsx
import {
  KYBSessionProvider,
  useKYBSession,
  CompanySearch,
  BusinessDetailsForm,
  DocumentUpload,
  DocumentPreview,
  UploadProgress,
  UBOCollection,
  UBOForm,
  UBOKYCTrigger,
  SubmissionStatus,
} from '@mallient/react/kyb'
```

#### useKYBSession

Reads and dispatches to the KYB state machine. Must be used inside `<KYBSessionProvider>`:

```tsx
import { KYBSessionProvider, useKYBSession } from '@mallient/react/kyb'

function MyStep() {
  const { state, dispatch } = useKYBSession()

  return (
    <button onClick={() => dispatch({ type: 'navigate', step: 'ubo_collection' })}>
      Skip to owners
    </button>
  )
}

function App() {
  return (
    <KYBSessionProvider>
      <MyStep />
    </KYBSessionProvider>
  )
}
```

#### KYB state machine actions

| Action | Effect |
|--------|--------|
| `{ type: 'company.selected', company }` | Advance to `business_details` |
| `{ type: 'details.submitted', details }` | Advance to `document_upload` |
| `{ type: 'documents.uploaded', documents }` | Advance to `ubo_collection` |
| `{ type: 'ubos.updated', ubos }` | Update UBO list (stays on current step) |
| `{ type: 'ubo.kyc.complete', uboId, result }` | Mark UBO KYC as complete |
| `{ type: 'ubo.kyc.failed', uboId }` | Mark UBO KYC as failed |
| `{ type: 'review.start' }` | Advance to `review` |
| `{ type: 'submit.start' }` | Set `submitting: true` |
| `{ type: 'submit.complete' }` | Advance to `submitted` |
| `{ type: 'submit.failed', reason }` | Set `error` message |
| `{ type: 'navigate', step }` | Jump to any step |

---

## Core utilities

Import from `@mallient/react/core`:

```tsx
import {
  MallientProvider,    // single root provider for all Mallient widgets
  SessionProvider,     // low-level WebSocket session context (used by MallientProvider)
  useSession,          // consume session state inside a MallientProvider
  useWebSocket,        // low-level WebSocket hook with auto-reconnect
  StatusBadge,         // status pill component
  UploadZone,          // drag-and-drop file target
  ErrorBoundary,       // class component error boundary
  injectStyles,        // CSS-in-JS helper (used internally)
  buildThemeVars,      // converts KYCTheme → CSS custom property map
} from '@mallient/react/core'
```

#### UploadZone

```tsx
import { UploadZone } from '@mallient/react/core'

<UploadZone
  accept=".pdf,.jpg,.png"
  multiple
  hint="PDF, JPG or PNG · Max 10 MB"
  onFiles={(files: File[]) => handleFiles(files)}
/>
```

#### ErrorBoundary

```tsx
import { ErrorBoundary } from '@mallient/react/core'

<ErrorBoundary
  fallback={<p>Something went wrong.</p>}
  onError={(err, info) => logError(err, info)}
>
  <MallientKYC ... />
</ErrorBoundary>
```

---

## TypeScript types

All types are exported from `@mallient/react/core`:

```ts
import type {
  // KYC
  VerificationResult,      // { sessionId, status, checks, completedAt }
  SessionState,            // discriminated union of all KYC statuses
  WSMessage,               // WebSocket message union
  KYCTheme,                // theme override object

  // KYB
  CompanySearchResult,     // { companyNumber, companyName, jurisdiction, status, ... }
  BusinessDetails,         // { legalName, registrationNumber, jurisdiction, address, businessType }
  UploadedDocument,        // { id, name, type, size, uploadedAt, url? }
  UBO,                     // { id, firstName, lastName, ownershipPercentage, kycStatus, ... }
  KYBState,                // full KYB wizard state
  KYBAction,               // all dispatchable actions
  KYBStep,                 // 'company_search' | 'business_details' | ... | 'submitted'
} from '@mallient/react/core'
```

---

## Development

```bash
# Install dependencies
npm install

# Start the component demo (hot-reload dev server)
npm run dev

# Type-check without emitting
npm run typecheck

# Lint
npm run lint

# Build the distributable package
npm run build
```

The dev server at `http://localhost:5173` runs an interactive demo with three tabs:

- **KYB Flow** — fully interactive wizard with mock handlers
- **KYC States** — all verification states rendered without a live backend
- **Legacy Components** — original `KYCWidget` form and `Button`

---

## Peer requirements

| Package | Version |
|---------|---------|
| `react` | `^18` or `^19` |
| `react-dom` | `^18` or `^19` |


## License

MIT