npm package: @swan-admin/swan-native-component
Description: swan's react native app
Author: paras
Latest version: 0.1.2

# @swan-admin/swan-native-component

The official React Native SDK for Swan's Body & Face Scanning suite. Drop-in components for onboarding, device calibration, and scanning — no WebView setup required.

---

## Table of Contents

- [Installation](#installation)
- [Setup & Permissions](#setup--permissions)
- [Quick Start](#quick-start)
- [API Reference](#api-reference)
- [Components](#components)
  - [Onboarding](#onboarding)
  - [FocalLength](#focallength)
  - [Educational](#educational)
  - [BodyScan](#bodyscan)
  - [FaceScan](#facescan)
- [Interfaces](#interfaces)
- [Full Integration Example](#full-integration-example)
- [Troubleshooting](#troubleshooting)

---

## Installation

```sh
npm install @swan-admin/swan-native-component react-native-webview react-native-safe-area-context
# or
yarn add @swan-admin/swan-native-component react-native-webview react-native-safe-area-context
```

For iOS, install pods:

```sh
cd ios && pod install
```

---

## Setup & Permissions

The SDK uses the device camera for scanning. You must add native permissions.

### iOS (`ios/<ProjectName>/Info.plist`)

```xml
<key>NSCameraUsageDescription</key>
<string>We need camera access to perform the body scan measurements.</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need microphone access to initialize the video stream.</string>
```

> **Note:** `NSMicrophoneUsageDescription` is required by iOS to initialize the video stream, even if audio is not recorded.

### Android (`android/app/src/main/AndroidManifest.xml`)

```xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.webkit.PermissionRequest" />
```

After editing permissions, **rebuild your native app**:

```sh
npx react-native run-ios   # or run-android
```

---

## Quick Start

```tsx
import { SwanWebView } from '@swan-admin/swan-native-component';

<SwanWebView
  component="Onboarding"
  config={{
    steps: [
      { type: 'gender' },
      { type: 'email' },
      { type: 'name' },
      { type: 'height' },
    ],
  }}
  onCallback={(name, data) => {
    if (name === 'onComplete') {
      console.log('User data:', data);
    }
  }}
/>
```

---

## API Reference

### `<SwanWebView />`

The single component exposed by this package. It handles all WebView setup, bridge communication, and lifecycle management internally.

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `component` | `SwanComponentType` | Yes | Which component to render. One of: `"Onboarding"`, `"FocalLength"`, `"Educational"`, `"BodyScan"`, `"FaceScan"` |
| `config` | `object` | No | Props passed to the rendered component (see each component below) |
| `onCallback` | `(name: string, data: any) => void` | No | Handler for events emitted by the component |

---

## Components

### Onboarding

Collects user info (email, name, gender, height) in a step-by-step flow.

**Config:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `steps` | `Step[]` | Yes | Steps to display |
| `config` | `Config` | No | Styling/branding config |

**Callbacks:**

| Name | Payload | Description |
|------|---------|-------------|
| `onComplete` | `Record<string, any>` | All collected values: `{ email, name, gender, height }` |
| `onDeviceDetected` | `FocalLengthOutput` | Device info auto-detected during onboarding |
| `onStepComplete` | `{ type, value }` | Fired after each individual step |

**Example:**

```tsx
<SwanWebView
  component="Onboarding"
  config={{
    steps: [
      { type: 'gender' },
      { type: 'email' },
      { type: 'name' },
      { type: 'height' },
    ],
    config: {
      logo: 'https://example.com/logo.png',
      language: 'English',
    },
  }}
  onCallback={(name, data) => {
    if (name === 'onComplete') {
      console.log('User data:', data);
    }
    if (name === 'onStepComplete') {
      console.log(`Step ${data.type} finished:`, data.value);
    }
  }}
/>
```

---

### FocalLength

Detects the device camera's focal length, required for accurate body measurements.

**Config:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `config` | `Config` | No | Styling/branding config |

**Callbacks:**

| Name | Payload | Description |
|------|---------|-------------|
| `onComplete` | `FocalLengthOutput` | `{ brandName, modelName, focalLength }` |

**Example:**

```tsx
<SwanWebView
  component="FocalLength"
  config={{}}
  onCallback={(name, data) => {
    if (name === 'onComplete') {
      console.log('Focal length:', data.focalLength);
      console.log('Model:', data.modelName);
    }
  }}
/>
```

---

### Educational

Shows tutorial/instructions before the scanning process.

**Config:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `gender` | `"male" \| "female"` | No | Customizes tutorial content |
| `sections` | `("full" \| "body" \| "face")[]` | No | Which sections to show |
| `config` | `Config` | No | Styling/branding config |
| `isCustom` | `boolean` | No | Custom mode flag |

**Callbacks:**

| Name | Payload | Description |
|------|---------|-------------|
| `onComplete` | none | User finished the tutorial |

**Example:**

```tsx
<SwanWebView
  component="Educational"
  config={{
    gender: 'male',
    sections: ['body', 'face'],
  }}
  onCallback={(name) => {
    if (name === 'onComplete') {
      // Navigate to scan
    }
  }}
/>
```

---

### BodyScan

The core body measurement camera interface. Supports two modes:

**Active Scan** — `token` and `userDetails` are required:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `token` | `string` | **Yes** | Auth token |
| `userDetails` | `UserDetails` | **Yes** | User info for scan |
| `config` | `Config` | No | Styling/branding config |
| `isError` | `false` | No | Must be `false` or omitted |
| `isSuccess` | `false` | No | Must be `false` or omitted |

**Terminal State** — renders only the error/success UI, `token` and `userDetails` become optional:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `token` | `string` | No | Auth token (optional in terminal state) |
| `userDetails` | `UserDetails` | No | User info (optional in terminal state) |
| `config` | `Config` | No | Styling/branding config |
| `isError` | `true` | No | Show error UI |
| `isSuccess` | `true` | No | Show success UI |

**Callbacks:**

| Name | Payload | Description |
|------|---------|-------------|
| `onScanStart` | none | Scan started |
| `onUploadStart` | none | Video upload started |
| `onUploadEnd` | none | Video upload finished |
| `onMeasurementSocketStart` | none | Measurement WebSocket opened |
| `onMeasurementSocketClose` | none | Measurement WebSocket closed |
| `onIntermediateScanSuccess` | `data` | Intermediate result received |
| `onScanSuccess` | `data` | Final scan success with measurements |
| `onScanError` | `error` | Scan failed |
| `onRetry` | none | User requested retry |
| `onComplete` | none | Component flow finished |

**Example (Active Scan):**

```tsx
<SwanWebView
  component="BodyScan"
  config={{
    token: 'your-auth-token',
    userDetails: {
      email: 'user@example.com',
      userName: 'John',
      shopDomain: 'yourstore.myshopify.com',
      gender: 'male',
      heightInCm: 175,
      scanType: 'clothing_scan',
      deviceFocalLength: 26,
      deviceModelName: 'iPhone 14',
    },
    config: {
      logo: 'https://example.com/logo.png',
    },
  }}
  onCallback={(name, data) => {
    if (name === 'onScanSuccess') {
      console.log('Measurements:', data);
    } else if (name === 'onScanError') {
      console.error('Scan failed:', data);
    }
  }}
/>
```

**Example (Terminal State — show success UI):**

```tsx
<SwanWebView
  component="BodyScan"
  config={{
    isSuccess: true,
  }}
/>
```

**Example (Terminal State — show error UI):**

```tsx
<SwanWebView
  component="BodyScan"
  config={{
    isError: true,
  }}
/>
```

---

### FaceScan

Facial analysis via the front camera. Supports two modes:

**Active Scan** — `token` and `userDetails` are required:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `token` | `string` | **Yes** | Auth token |
| `userDetails` | `FaceScanMetaData` | **Yes** | User info for scan |
| `config` | `Config` | No | Styling/branding config |
| `isError` | `false` | No | Must be `false` or omitted |
| `isSuccess` | `false` | No | Must be `false` or omitted |

**Terminal State** — renders only the error/success UI, `token` and `userDetails` become optional:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `token` | `string` | No | Auth token (optional in terminal state) |
| `userDetails` | `FaceScanMetaData` | No | User info (optional in terminal state) |
| `config` | `Config` | No | Styling/branding config |
| `isError` | `true` | No | Show error UI |
| `isSuccess` | `true` | No | Show success UI |

**Callbacks:**

| Name | Payload | Description |
|------|---------|-------------|
| `onScanStart` | none | Scan started |
| `onUploadStart` | none | Video upload started |
| `onUploadEnd` | none | Video upload finished |
| `onMeasurementSocketStart` | none | Measurement WebSocket opened |
| `onMeasurementSocketClose` | none | Measurement WebSocket closed |
| `onScanSuccess` | `data` | Scan success |
| `onScanError` | `error` | Scan failed |
| `onRetry` | none | User requested retry |

**Example (Active Scan):**

```tsx
<SwanWebView
  component="FaceScan"
  config={{
    token: 'your-auth-token',
    userDetails: {
      email: 'user@example.com',
      shopDomain: 'yourstore.myshopify.com',
      gender: 'male',
      deviceFocalLength: 26,
    },
    config: {
      logo: 'https://example.com/logo.png',
    },
  }}
  onCallback={(name, data) => {
    if (name === 'onScanSuccess') {
      console.log('Face scan done:', data);
    } else if (name === 'onScanError') {
      console.error('Face scan failed:', data);
    }
  }}
/>
```

**Example (Terminal State — show success UI):**

```tsx
<SwanWebView
  component="FaceScan"
  config={{
    isSuccess: true,
  }}
/>
```

**Example (Terminal State — show error UI):**

```tsx
<SwanWebView
  component="FaceScan"
  config={{
    isError: true,
  }}
/>
```

---

## Interfaces

### UserDetails (for BodyScan)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `email` | `string` | Yes | User email |
| `userName` | `string` | No | User display name |
| `shopDomain` | `string` | Yes | Shopify store domain |
| `gender` | `"male" \| "female"` | Yes | User gender |
| `heightInCm` | `number` | Yes | User height in centimeters |
| `scanType` | `string` | Yes | e.g. `"clothing_scan"` |
| `deviceFocalLength` | `number` | Yes | From FocalLength step |
| `deviceModelName` | `string` | Yes | From FocalLength step |
| `callbackUrl` | `string` | No | Webhook URL for results |

### FaceScanMetaData (for FaceScan)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `email` | `string` | Yes | User email |
| `shopDomain` | `string` | Yes | Shopify store domain |
| `gender` | `"male" \| "female"` | Yes | User gender |
| `deviceFocalLength` | `number` | Yes | From FocalLength step |
| `callbackUrl` | `string` | No | Webhook URL for results |

### FocalLengthOutput

| Field | Type | Description |
|-------|------|-------------|
| `brandName` | `string` | Device brand |
| `modelName` | `string` | Device model |
| `focalLength` | `number` | Camera focal length |

### Config

| Field | Type | Description |
|-------|------|-------------|
| `style` | `StyleConfig` | Custom styling |
| `logo` | `string` | Logo URL |
| `loader` | `string` | Custom loader URL |
| `language` | `string` | One of: `"English"`, `"Hindi"`, `"German"`, `"French"`, `"Spanish"`, `"Arabic"`, `"Italian"` |

### Step

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | `string` | Yes | `"email"`, `"name"`, `"height"`, or `"gender"` |
| `value` | `any` | No | Pre-filled value |
| `order` | `number` | No | Display order |
| `isVisible` | `boolean` | No | Show/hide step |

---

## Full Integration Example

The typical integration is a state machine that moves through the steps sequentially, passing collected data forward.

```tsx
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { SwanWebView } from '@swan-admin/swan-native-component';

enum Step {
  START,
  ONBOARDING,
  FOCAL_LENGTH,
  EDUCATIONAL,
  BODY_SCAN,
  FACE_SCAN,
  FINISH,
}

export default function App() {
  const [step, setStep] = useState(Step.START);
  const [userData, setUserData] = useState<any>({});
  const [deviceData, setDeviceData] = useState<any>({});

  if (step === Step.START) {
    return (
      <View style={styles.center}>
        <Text style={styles.title}>Swan Demo</Text>
        <TouchableOpacity style={styles.button} onPress={() => setStep(Step.ONBOARDING)}>
          <Text style={styles.buttonText}>Start</Text>
        </TouchableOpacity>
      </View>
    );
  }

  if (step === Step.ONBOARDING) {
    return (
      <SwanWebView
        component="Onboarding"
        config={{
          steps: [
            { type: 'gender' },
            { type: 'email' },
            { type: 'name' },
            { type: 'height' },
          ],
        }}
        onCallback={(name, data) => {
          if (name === 'onComplete') {
            setUserData(data);
            setStep(Step.FOCAL_LENGTH);
          }
        }}
      />
    );
  }

  if (step === Step.FOCAL_LENGTH) {
    return (
      <SwanWebView
        component="FocalLength"
        config={{}}
        onCallback={(name, data) => {
          if (name === 'onComplete') {
            setDeviceData(data);
            setStep(Step.EDUCATIONAL);
          }
        }}
      />
    );
  }

  if (step === Step.EDUCATIONAL) {
    return (
      <SwanWebView
        component="Educational"
        config={{ gender: userData.gender }}
        onCallback={(name) => {
          if (name === 'onComplete') {
            setStep(Step.BODY_SCAN);
          }
        }}
      />
    );
  }

  if (step === Step.BODY_SCAN) {
    return (
      <SwanWebView
        component="BodyScan"
        config={{
          token: 'your-auth-token',
          userDetails: {
            email: userData.email,
            userName: userData.name,
            shopDomain: 'yourstore.myshopify.com',
            gender: userData.gender,
            heightInCm: userData.height,
            scanType: 'clothing_scan',
            deviceFocalLength: deviceData.focalLength,
            deviceModelName: deviceData.modelName,
          },
        }}
        onCallback={(name, data) => {
          if (name === 'onScanSuccess') {
            setStep(Step.FACE_SCAN);
          }
        }}
      />
    );
  }

  if (step === Step.FACE_SCAN) {
    return (
      <SwanWebView
        component="FaceScan"
        config={{
          token: 'your-auth-token',
          userDetails: {
            email: userData.email,
            shopDomain: 'yourstore.myshopify.com',
            gender: userData.gender,
            deviceFocalLength: deviceData.focalLength,
          },
        }}
        onCallback={(name, data) => {
          if (name === 'onScanSuccess') {
            setStep(Step.FINISH);
          }
        }}
      />
    );
  }

  return (
    <View style={styles.center}>
      <Text style={styles.title}>Process Complete!</Text>
      <TouchableOpacity style={styles.button} onPress={() => setStep(Step.START)}>
        <Text style={styles.buttonText}>Start Again</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  center: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#fff' },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 20 },
  button: { backgroundColor: '#000', paddingHorizontal: 30, paddingVertical: 15, borderRadius: 30 },
  buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
});
```

### Recommended Flow

```
Onboarding -> FocalLength -> Educational -> BodyScan -> FaceScan
```

Each component's `onComplete` / `onScanSuccess` callback signals that you should render the next component.

---

## Troubleshooting

### Camera screen is black or "Permission Denied"

- **Check internet** — components load assets remotely.
- **Check OS permissions** — Go to Device Settings > Apps > [Your App] and toggle Camera ON.
- **Rebuild native app** — If you edited `Info.plist` or `AndroidManifest.xml`, you must rebuild.

### Video not loading on iOS

Ensure `NSMicrophoneUsageDescription` is in your `Info.plist`. iOS blocks the camera stream if the microphone permission description is missing, even if you don't record audio.

### Callbacks not firing

- Ensure your `config` object contains only JSON-serializable data (no functions).
- Listen to events via `onCallback`, not by passing functions inside `config`.

---

## License

MIT