# @finos_sdk/sdk-ekyc

[![npm version](https://badge.fury.io/js/%40finos_sdk%2Fsdk-ekyc.svg)](https://badge.fury.io/js/%40finos_sdk%2Fsdk-ekyc)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

React Native SDK for eKYC (electronic Know Your Customer) and eSign. Features include Vietnamese CCCD NFC reading, OCR, Liveness detection, Face matching, C06 residence verification, SMS OTP verification, and Electronic Signature (eSign) capabilities.

**Version**: 1.5.2

## Features

- **Unified eKYC Flow** - Complete verification flow with single method call
- **NFC Reading** - Read Vietnamese CCCD cards with NFC chips
- **OCR Processing** - Extract data from ID card images
- **Liveness Detection** - Verify user presence with selfie (Active/Passive modes)
- **Face Matching** - Compare selfie with ID card photo
- **C06 Verification** - Residence information verification
- **eSign** - Electronic signature support (PDF signing, Remote signing)
- **SMS OTP** - Integrated SMS OTP verification
- **Flexible Configuration** - Custom styling, capture button colors, and retry options
- **TypeScript Support** - Full type definitions included

## Requirements

- React Native 0.70.0+
- Android 24+ / API Level 24+
- iOS 14.0+ (Swift 5.7+)
- NFC and/or Camera hardware depending on modules used
- Runtime permissions for Camera and/or NFC

## Installation

```bash
npm install @finos_sdk/sdk-ekyc
# or
yarn add @finos_sdk/sdk-ekyc
```

---

## Quick Start

### 1. Import the SDK

```typescript
import {
  FinosEKYC,
  FinosESign,
  getEkycError,
  SDKFaceDetectStatus,
  SDKFlowType,
  AppIDType,
} from '@finos_sdk/sdk-ekyc';

import type {
  NfcConfig,
  C06Config,
  OcrConfig,
  LivenessConfig,
  FaceServiceConfig,
  SmsOtpConfig,
  StartEkycUIResult,
  EKYCError,
} from '@finos_sdk/sdk-ekyc';
```

### 2. Initialize SDK

```typescript
// Initialize the SDK (required before using any method)
await FinosEKYC.initialize();

// Check SDK info
const info = await FinosEKYC.getSDKInfo();
console.log(info); // { name, version, buildNumber, platform, isInitialized }

// Set transaction ID (optional)
await FinosEKYC.setTransactionId('your-transaction-id');
```

---

## Complete eKYC Flow (startEkycUI)

The recommended way to use the SDK. Handles the complete verification flow with a single method call.

### Method Signature

```typescript
FinosEKYC.startEkycUI(
  appKey: string,
  flowSDK: SDKFlowType[],
  language: string,
  transactionId: string,
  appKeyConfig: AppKeyConfig,
  optionConfig?: OptionConfig,
  styleConfig?: StyleConfig,
): Promise<StartEkycUIResult>
```

### Full Example with All Configs

```typescript
const result = await FinosEKYC.startEkycUI(
  'your-main-app-key',

  // --- flowSDK: verification steps to execute in order ---
  ['OCR', 'NFC', 'LIVENESS'],

  // --- language ---
  'vi',  // 'vi' | 'en'

  // --- transactionId ---
  'txn-20260319-001',

  // --- appKeyConfig: API keys for each module ---
  {
    appKey: 'your-main-app-key',
    appKeyNfc: 'your-nfc-key',
    appKeyOcr: 'your-ocr-key',
    appKeyLiveness: 'your-liveness-key',
    appKeyC06: 'your-c06-key',
    appKeyFaceService: 'your-face-key',
  },

  // --- optionConfig: SDK behavior settings ---
  {
    baseUrl: 'https://api.example.com',       // Custom API base URL
    countMaxRetry: 3,                          // Max retry attempts (default: 3)
    language: 'vi',                            // Override language
    switchFrontCamera: true,                   // Use front camera (default: false)
    isActiveLiveness: true,                    // Active liveness mode (default: true)
    autoCapture: true,                         // Auto capture (default: true)
    forceCaptureTimeout: 30,                   // Force capture timeout in seconds
    isShowCameraFont: true,                    // Show camera guide text
    customActions: [                           // Custom liveness actions
      SDKFaceDetectStatus.LEFT,
      SDKFaceDetectStatus.RIGHT,
      SDKFaceDetectStatus.STRAIGHT,
    ],
    activeActionCount: 3,                      // Random action count 1-10 (if customActions is null)
    appIDType: AppIDType.VIKKI,                // App ID type: NONE | HD_BANK | VIKKI
  },

  // --- styleConfig: UI customization ---
  {
    // Global defaults
    textSize: 14,                              // Default text size (sp)
    textFont: '',                              // Font resource name (e.g. 'roboto_medium')
    textColor: 0xFF333333,                     // Default text color (ARGB int)
    statusBarBackground: undefined,            // Status bar drawable resource ID
    backIcon: undefined,                       // Back button drawable resource ID

    // Per-element text styles (override global defaults)
    titleStyle: {                              // Screen title ("CHUP MAT TRUOC", "Chup chan dung")
      textSize: 20,
      textFont: 'roboto_bold',
      textColor: 0xFF142630,
    },
    toolbarStyle: {                            // Toolbar/navigation bar title
      textSize: 18,
      textFont: '',
      textColor: 0xFF007AFF,
    },
    instructionStyle: {                        // User instruction text
      textSize: 16,
      textFont: '',
      textColor: 0xFF666666,
    },
    errorStyle: {                              // Error message text
      textSize: 14,
      textFont: '',
      textColor: 0xFFFF3B30,
    },
    successStyle: {                            // Success message text
      textSize: 14,
      textFont: '',
      textColor: 0xFF34C759,
    },
    warningStyle: {                            // Warning message text
      textSize: 14,
      textFont: '',
      textColor: 0xFFFF9500,
    },

    // Capture button colors (direct color int, ARGB)
    captureButtonColor: 0xFF00A86F,            // Capture button color when enabled
    captureButtonDisabledColor: 0xFFCCCCCC,    // Capture button color when disabled
  },
);
```

### Result

```typescript
interface StartEkycUIResult {
  status: 'success';
  event: string;
  data?: string;
  transactionId?: string;
  imageFace?: string;            // Base64 selfie/liveness image
  imageOcrFront?: string;        // Base64 OCR front image
  imageOcrBack?: string;         // Base64 OCR back image
  imageFacePath?: string;        // Absolute path to selfie file
  imageOcrFrontPath?: string;    // Absolute path to OCR front file
  imageOcrBackPath?: string;     // Absolute path to OCR back file
}
```

---

## Configuration Reference

### AppKeyConfig

API keys for each eKYC module.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `appKey` | `string` | Yes | Main app key |
| `appKeyNfc` | `string` | Yes | NFC module key |
| `appKeyOcr` | `string` | Yes | OCR module key |
| `appKeyLiveness` | `string` | Yes | Liveness module key |
| `appKeyC06` | `string` | Yes | C06 module key |
| `appKeyFaceService` | `string` | Yes | Face service module key |

```typescript
const appKeyConfig = {
  appKey: 'your-main-app-key',
  appKeyNfc: 'your-nfc-key',
  appKeyOcr: 'your-ocr-key',
  appKeyLiveness: 'your-liveness-key',
  appKeyC06: 'your-c06-key',
  appKeyFaceService: 'your-face-key',
};
```

### OptionConfig

SDK behavior and runtime settings.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `baseUrl` | `string` | `undefined` | Custom API base URL |
| `countMaxRetry` | `number` | `3` | Max retry attempts on failure |
| `language` | `string` | `undefined` | Override language (`'vi'` or `'en'`) |
| `switchFrontCamera` | `boolean` | `false` | Use front camera for liveness/face |
| `isActiveLiveness` | `boolean` | `true` | Enable active liveness detection |
| `autoCapture` | `boolean` | `true` | Auto capture when conditions met |
| `forceCaptureTimeout` | `number` | `0` | Force capture timeout in seconds (0 = disabled) |
| `isShowCameraFont` | `boolean` | `true` | Show instruction text on camera screen |
| `customActions` | `SDKFaceDetectStatus[]` | `undefined` | Custom liveness action sequence |
| `activeActionCount` | `number` | `2` | Number of random actions (1-10, used when `customActions` is null) |
| `appIDType` | `AppIDType` | `NONE` | App ID type for branding: `NONE`, `HD_BANK`, or `VIKKI` |

```typescript
// Minimal
const optionConfig = { countMaxRetry: 3 };

// Full
const optionConfig = {
  baseUrl: 'https://api.example.com',
  countMaxRetry: 3,
  language: 'vi',
  switchFrontCamera: true,
  isActiveLiveness: true,
  autoCapture: true,
  forceCaptureTimeout: 30,
  isShowCameraFont: true,
  customActions: [
    SDKFaceDetectStatus.LEFT,
    SDKFaceDetectStatus.RIGHT,
    SDKFaceDetectStatus.STRAIGHT,
  ],
  activeActionCount: 3,
  appIDType: AppIDType.VIKKI,
};
```

### AppIDType

Determines the branding/skin of SDK screens.

| Value | Description |
|-------|-------------|
| `NONE` | Default, no branding |
| `HD_BANK` | HD Bank branding |
| `VIKKI` | Vikki branding |

```typescript
import { AppIDType } from '@finos_sdk/sdk-ekyc';

// In optionConfig
{ appIDType: AppIDType.VIKKI }
```

### StyleConfig

UI appearance customization. All color values use ARGB integer format (e.g., `0xFFRRGGBB`).

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `textSize` | `number` | `14` | Default text size (sp) |
| `textFont` | `string` | `''` | Default font resource name (e.g., `'roboto_medium'`) |
| `textColor` | `number` | `0xFF000000` | Default text color (ARGB) |
| `statusBarBackground` | `number` | `undefined` | Status bar background drawable resource ID |
| `backIcon` | `number` | `undefined` | Back button icon drawable resource ID |
| `titleStyle` | `TextStyle` | `undefined` | Style for screen titles |
| `toolbarStyle` | `TextStyle` | `undefined` | Style for toolbar/navigation title |
| `instructionStyle` | `TextStyle` | `undefined` | Style for instruction text |
| `errorStyle` | `TextStyle` | `undefined` | Style for error messages |
| `successStyle` | `TextStyle` | `undefined` | Style for success messages |
| `warningStyle` | `TextStyle` | `undefined` | Style for warning messages |
| `captureButtonColor` | `number` | `undefined` | Capture button color when enabled (ARGB) |
| `captureButtonDisabledColor` | `number` | `undefined` | Capture button color when disabled (ARGB) |

#### TextStyle (sub-config)

Each text style can override the global defaults. `null`/`undefined` fields fall back to the parent `StyleConfig` defaults.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `textSize` | `number` | Parent `textSize` | Text size (sp) |
| `textFont` | `string` | Parent `textFont` | Font resource name |
| `textColor` | `number` | Parent `textColor` | Text color (ARGB) |

```typescript
// Minimal - just change capture button color
const styleConfig = {
  captureButtonColor: 0xFF00A86F,
  captureButtonDisabledColor: 0xFFCCCCCC,
};

// Custom theme with all styles
const styleConfig = {
  textSize: 14,
  textFont: 'roboto_regular',
  textColor: 0xFF333333,
  titleStyle: {
    textSize: 20,
    textFont: 'roboto_bold',
    textColor: 0xFF142630,
  },
  toolbarStyle: {
    textSize: 18,
    textColor: 0xFF007AFF,
  },
  instructionStyle: {
    textSize: 16,
    textColor: 0xFF666666,
  },
  errorStyle: {
    textColor: 0xFFFF3B30,
  },
  successStyle: {
    textColor: 0xFF34C759,
  },
  warningStyle: {
    textColor: 0xFFFF9500,
  },
  captureButtonColor: 0xFF142630,
  captureButtonDisabledColor: 0xFFDDDDDD,
};
```

### SDKFlowType

Available flow step types for `startEkycUI`.

| Value | Description |
|-------|-------------|
| `'OCR'` | Scan ID card with camera (front/back) |
| `'NFC'` | Read NFC chip on CCCD |
| `'LIVENESS'` | Liveness detection (selfie) |

```typescript
// Common flows
const flow1 = ['OCR', 'NFC', 'LIVENESS'];          // Full eKYC
const flow2 = ['OCR', 'LIVENESS'];                  // Without NFC
const flow3 = ['LIVENESS'];                          // Liveness only
```

### SDKFaceDetectStatus

Available liveness actions for `customActions`.

| Value | Description |
|-------|-------------|
| `LEFT` | Turn head left |
| `RIGHT` | Turn head right |
| `UP` | Look up |
| `DOWN` | Look down |
| `STRAIGHT` | Look straight |
| `SMILE` | Smile |
| `BLINK` | Blink eyes |
| `TILT_LEFT` | Tilt head left |
| `TILT_RIGHT` | Tilt head right |
| `WINK_LEFT` | Wink left eye |
| `WINK_RIGHT` | Wink right eye |

```typescript
import { SDKFaceDetectStatus } from '@finos_sdk/sdk-ekyc';

const customActions = [
  SDKFaceDetectStatus.LEFT,
  SDKFaceDetectStatus.RIGHT,
  SDKFaceDetectStatus.SMILE,
];
```

---

## Individual Module APIs

### NFC Scanning

```typescript
const config: NfcConfig = {
  appKey: 'your-nfc-key',            // Required
  documentNumber: '012345678012',     // Required: CCCD number
  birthDate: '01011990',              // Required: format DDMMYYYY
  expireDate: '01012030',             // Required: format DDMMYYYY
  transactionId: 'txn-001',          // Optional
  facePathStorage: '/path/to/face',  // Optional: path to save face image
};

const result = await FinosEKYC.startNfcScan(config);
```

**NfcConfig**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `appKey` | `string` | Yes | NFC module API key |
| `documentNumber` | `string` | Yes | CCCD/CMND number |
| `birthDate` | `string` | Yes | Date of birth (DDMMYYYY) |
| `expireDate` | `string` | Yes | Expiry date (DDMMYYYY) |
| `transactionId` | `string` | No | Transaction ID |
| `facePathStorage` | `string` | No | File path to store extracted face image |

### OCR Processing

```typescript
const config: OcrConfig = {
  appKey: 'your-ocr-key',            // Required
  transactionId: 'txn-001',          // Required
  idImagePath: 'base64_image_data',  // Required: Base64 encoded image
  expectedDocumentSide: 'front',     // Required: 'front' or 'back'
};

const result = await FinosEKYC.startOcr(config);
```

**OcrConfig**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `appKey` | `string` | Yes | OCR module API key |
| `transactionId` | `string` | Yes | Transaction ID |
| `idImagePath` | `string` | Yes | Base64 encoded ID card image |
| `expectedDocumentSide` | `string` | Yes | `'front'` or `'back'` |

### Liveness Detection

```typescript
const config: LivenessConfig = {
  appKey: 'your-liveness-key',       // Required
  selfieImage: 'base64_image',       // Required: Base64 encoded selfie
  transactionId: 'txn-001',          // Optional
  switchFrontCamera: true,           // Optional: use front camera
  isActiveLiveness: true,            // Optional: active liveness mode
  autoCapture: true,                 // Optional: auto capture
  isShowCameraFont: true,            // Optional: show guide text
  forceCaptureTimeout: 30,           // Optional: force capture timeout (seconds)
  customActions: [                   // Optional: custom action sequence
    SDKFaceDetectStatus.LEFT,
    SDKFaceDetectStatus.RIGHT,
    SDKFaceDetectStatus.SMILE,
  ],
  activeActionCount: 3,              // Optional: random action count (1-10)
};

const result = await FinosEKYC.startLiveness(config);
```

**LivenessConfig**

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `appKey` | `string` | - | Liveness module API key (required) |
| `selfieImage` | `string` | - | Base64 encoded selfie image (required) |
| `transactionId` | `string` | `undefined` | Transaction ID |
| `switchFrontCamera` | `boolean` | `false` | Use front camera |
| `isActiveLiveness` | `boolean` | `false` | Enable active liveness mode |
| `autoCapture` | `boolean` | `true` | Auto capture when conditions met |
| `isShowCameraFont` | `boolean` | `true` | Show camera guide text |
| `forceCaptureTimeout` | `number` | `0` | Force capture timeout in seconds |
| `customActions` | `SDKFaceDetectStatus[]` | `undefined` | Custom action sequence (overrides random) |
| `activeActionCount` | `number` | `2` | Random actions count 1-10 (when `customActions` is null) |

### Face Comparison

```typescript
const config: FaceServiceConfig = {
  appKey: 'your-face-key',           // Required
  selfieImage: 'base64_selfie',      // Required: Base64 selfie image
  idImage: 'base64_id_image',        // Required: Base64 ID card image
  transactionId: 'txn-001',          // Optional
  appKeyFaceService: 'alt-key',      // Optional: alternative key
};

const result = await FinosEKYC.startFaceCompare(config);
```

**FaceServiceConfig**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `appKey` | `string` | Yes | Face service API key |
| `selfieImage` | `string` | Yes | Base64 encoded selfie image |
| `idImage` | `string` | Yes | Base64 encoded ID card portrait |
| `transactionId` | `string` | No | Transaction ID |
| `appKeyFaceService` | `string` | No | Alternative face service key |

### C06 Verification

```typescript
const config: C06Config = {
  appKey: 'your-c06-key',            // Required
  sod: 'nfc_sod_data',               // Required: SOD data from NFC
  idCardNumber: '012345678012',       // Required: CCCD number
  recentLocation: 'Ha Noi',          // Optional: recent location
  transactionId: 'txn-001',          // Optional
  deviceType: 'android',             // Optional
};

const result = await FinosEKYC.checkC06(config);
```

**C06Config**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `appKey` | `string` | Yes | C06 module API key |
| `sod` | `string` | Yes | SOD data from NFC scan |
| `idCardNumber` | `string` | Yes | CCCD/CMND number |
| `recentLocation` | `string` | No | Recent location for verification |
| `transactionId` | `string` | No | Transaction ID |
| `deviceType` | `string` | No | Device type identifier |

---

## SMS OTP Integration

```typescript
const otpConfig: SmsOtpConfig = {
  appKey: 'your-api-key',            // Required: X-FinOS-Api-Key
  phoneNumber: '+84901234567',       // Required: with country code
  referenceId: 'ref-001',            // Required: unique reference
  purpose: 'transaction',            // Optional (default: 'transaction')
  requestId: undefined,              // Required for verify/resend (from send response)
};

// 1. Send OTP
const sendResult = await FinosEKYC.sendOtp(otpConfig);
console.log('Request ID:', sendResult.requestId);

// 2. Verify OTP (use requestId from send response)
otpConfig.requestId = sendResult.requestId;
const verifyResult = await FinosEKYC.verifyOtp(otpConfig, '123456');

// 3. Resend OTP
const resendResult = await FinosEKYC.resendOtp(otpConfig);
```

**SmsOtpConfig**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `appKey` | `string` | Yes | SMS OTP API key (X-FinOS-Api-Key) |
| `phoneNumber` | `string` | Yes | Phone number with country code (e.g., `+84901234567`) |
| `referenceId` | `string` | Yes | Unique reference ID |
| `purpose` | `string` | No | Purpose of OTP (default: `'transaction'`) |
| `requestId` | `string` | For verify/resend | Request ID from send response |

---

## eSign Integration

### Initialize

```typescript
// Initialize eSign SDK
await FinosESign.initializeESign('your-finos-token', false); // token, isProd

// Get SDK token
const token = await FinosESign.getSdkToken(
  '012345678012',     // CCCD number
  'Nguyen Van A',     // Full name
  'device-id-123',    // Device ID
);
```

### Open Session

```typescript
const session = await FinosESign.openSessionId(
  'access-token',     // Access token (or null)
  'username',         // Username (or null)
  true,               // Remember me (or null)
);
console.log(session.deviceState, session.code, session.message);
```

### Device Registration

```typescript
const result = await FinosESign.registerDevice(
  '12345678',         // 8-digit recovery code
  '123456',           // 6-digit PIN code
  'fcm-token',        // Optional FCM token
);
```

### Certificate Management

```typescript
// List certificates
const { certs } = await FinosESign.listCerts(1, 10);
// certs: [{ serial, subject, validFrom, validTo }]

// Verify certificate
const verifyResult = await FinosESign.verifyCert('cert-serial');
```

### Sign Documents

```typescript
// Sign a PDF
const signResult = await FinosESign.signPdf(JSON.stringify(signRequest));

// Sign PDF at multiple positions
const multiResult = await FinosESign.signPdfMultiplePositions(JSON.stringify(signRequest));

// Confirm signature
const confirmResult = await FinosESign.confirmSign(
  'sign-request-id',  // Sign request ID
  '123456',           // PIN code
  'auth-id',          // Optional auth ID
  'auth-data',        // Optional auth data
  true,               // Confirm (default: true)
);
```

### Remote Signing

```typescript
// Register for remote signing
const regResult = await FinosESign.registerRemoteSigning(JSON.stringify(requestBody));

// Register and Confirm in one step
const compositeResult = await FinosESign.registerAndConfirm(
  JSON.stringify(requestBody),
  'base64-confirmation-doc',
);

// Send confirmation document
const docResult = await FinosESign.sendConfirmationDocument(JSON.stringify(requestBody));
```

### Authorization Management

```typescript
// Initialize authorize request
await FinosESign.initAuthorize(
  'cert-serial',      // Certificate serial
  10,                 // Quantity (signing count)
  3600,               // Time (seconds until expiry)
  'Authorization msg',// Message
);

// List authorize requests
const { requests } = await FinosESign.listAuthorize(1, 10, 'ACTIVE');

// Register/Confirm authorize
await FinosESign.registerAuthorize(
  'auth-id',
  'auth-data',
  'authorize-request-id',
  '123456',           // PIN code
  true,               // Confirm
);
```

---

## Event Listeners

### NFC Events

```typescript
FinosEKYC.onNfcScanStart((data) => console.log('NFC scan started', data));
FinosEKYC.onNfcScanSuccess((data) => console.log('NFC data:', data));
FinosEKYC.onNfcError((error) => console.error('NFC error:', error.code, error.message));
```

### OCR Events

```typescript
FinosEKYC.onOcrSuccess((data) => console.log('OCR result:', data));
FinosEKYC.onOcrError((error) => console.error('OCR error:', error.code, error.message));
```

### Liveness Events

```typescript
FinosEKYC.onLivenessSuccess((data) => console.log('Liveness result:', data));
FinosEKYC.onLivenessError((error) => console.error('Liveness error:', error.code, error.message));
```

### Face Compare Events

```typescript
FinosEKYC.onFaceCompareSuccess((data) => console.log('Face match:', data));
FinosEKYC.onFaceCompareError((error) => console.error('Face error:', error.code, error.message));
```

### C06 Events

```typescript
FinosEKYC.onC06Success((data) => console.log('C06 result:', data));
FinosEKYC.onC06Error((error) => console.error('C06 error:', error.code, error.message));
```

### SMS OTP Events

```typescript
FinosEKYC.onSmsOtpSendSuccess((data) => console.log('OTP sent:', data));
FinosEKYC.onSmsOtpResendSuccess((data) => console.log('OTP resent:', data));
FinosEKYC.onSmsOtpError((error) => console.error('OTP error:', error.code, error.message));
```

### eSign Events

```typescript
FinosESign.onESignInitSuccess((data) => console.log('eSign init:', data));
FinosESign.onESignOpenSessionSuccess((data) => console.log('Session:', data));
FinosESign.onESignRegisterDeviceSuccess((data) => console.log('Device registered:', data));
FinosESign.onESignListCertsSuccess((data) => console.log('Certs:', data));
FinosESign.onESignSignPdfSuccess((data) => console.log('PDF signed:', data));
FinosESign.onESignError((error) => console.error('eSign error:', error.code, error.message));
```

### Cleanup

```typescript
// Remove all event listeners when done
FinosEKYC.removeAllListeners();
FinosESign.removeAllListeners();
```

---

## Error Handling

All errors follow a unified `EKYCError` format:

```typescript
interface EKYCError {
  event: string;     // Error event name (e.g., 'FACE_ERROR', 'NFC_ERROR')
  code: string;      // Error code (e.g., '110', '200', '999')
  message: string;   // Human-readable message
}
```

### Usage

```typescript
import { getEkycError, fromCode, getLocalizedMessage, USER_CANCEL } from '@finos_sdk/sdk-ekyc';

try {
  const result = await FinosEKYC.startEkycUI(/* ... */);
} catch (error) {
  const err = getEkycError(error);
  console.error(`[${err.event}] Code: ${err.code}, Message: ${err.message}`);

  // Check for user cancellation
  if (err.code === '999' || err.code === USER_CANCEL.code) {
    console.log('User cancelled the flow');
    return;
  }

  // Lookup error details by code
  const errorInfo = fromCode(err.code);
  if (errorInfo) {
    console.log('VI:', getLocalizedMessage(errorInfo, 'vi'));
    console.log('EN:', getLocalizedMessage(errorInfo, 'en'));
  }
}
```

### Common Error Codes

| Code | Constant | Description |
|------|----------|-------------|
| `999` | `USER_CANCEL` | User cancelled the flow |
| `000` | `ERROR_UNKNOWN` | Unknown error |
| `0` | `SDK_START_ERROR` | Failed to start SDK (check key) |
| `001` | `OCR_UNRECOGNIZED_ID_CARD` | Cannot recognize ID card |
| `110` | `LIVENESS_ERROR` | Liveness detection error |
| `109` | `FACE_ERROR` | Face comparison failed |
| `200` | `SCAN_NFC_ERROR` | NFC scan failed |
| `203` | `C06_ERROR` | C06 verification error |
| `209` | `NFC_USER_CANCEL` | User cancelled NFC scan |
| `300` | `SMS_OTP_ERROR` | SMS OTP error |

---

## Lifecycle Management

Handle app lifecycle events for NFC:

```typescript
import { AppState } from 'react-native';

useEffect(() => {
  const subscription = AppState.addEventListener('change', (state) => {
    if (state === 'active') {
      FinosEKYC.onResume();
    } else if (state === 'background') {
      FinosEKYC.onPause();
    }
  });

  return () => {
    subscription.remove();
    FinosEKYC.removeAllListeners();
  };
}, []);
```

---

## What's New

### v1.4.2
- `AppIDType` enum (NONE, HD_BANK, VIKKI) for branding/skin selection
- `captureButtonColor` and `captureButtonDisabledColor` in StyleConfig
- `USER_CANCEL` error code (999) for user cancellation detection
- User cancellation callback support across all modules
- Android responsive layouts for camera preview screens
- Refactored StyleConfig JSON parsing with proper JSONObject

### v1.4.1
- Full iOS native bridge support
- eSign module with PDF signing, remote signing, authorization
- SMS OTP integration
- Active Liveness with custom actions
