npm package: @finclusionaibuild/liveness-sdk
Description: AWS Face Liveness SDK for React applications.
Author: Finclusion
Latest version: 1.0.7

# @finclusionaibuild/liveness-sdk

A comprehensive Face Liveness Detection SDK for React applications. This SDK provides biometric face verification capabilities with a mobile-responsive UI.

## Features

- ✅ **Face Liveness Detection** - Real-time face verification.
- ✅ **Mobile Responsive** - Optimized for mobile, tablet, and desktop devices
- ✅ **Multiple Components** - `LivenessDetector`, `ExternalLiveness`, `PaymentLiveness`
- ✅ **TypeScript Support** - Full TypeScript definitions included
- ✅ **Error Handling** - Comprehensive error handling and retry mechanisms
- ✅ **Customizable** - Custom logos, instruction text, and styling
- ✅ **React Native Ready** - Works with React Native via WebView

## Installation

```bash
npm install @finclusionaibuild/liveness-sdk
```

## Peer Dependencies

The SDK requires these peer dependencies to be installed in your project:

```bash
npm install react@^18.0.0 react-dom@^18.0.0 @tanstack/react-query@^5.39.0
```

## Prerequisites

1. **React Query Setup**: You must wrap your application (or the component using the SDK) with `QueryClientProvider`:

```tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* Your app */}
    </QueryClientProvider>
  );
}
```

2. **CSS Import**: You must import the SDK's stylesheet:

```tsx
import "@finclusionaibuild/liveness-sdk/styles.css";
```

## Quick Start (Web)

### Basic Example

```tsx
import React from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { LivenessDetector } from "@finclusionaibuild/liveness-sdk";
import "@finclusionaibuild/liveness-sdk/styles.css";

const queryClient = new QueryClient();

function App() {
  const config = {
    apiBaseUrl: "https://api.example.com",
    getAuthToken: () => localStorage.getItem("authToken"),
    endpoints: {
      createSession: "/api/liveliness",
      verify: "/api/liveliness/{sessionId}",
    },
  };

  const handleComplete = () => {
    console.log("Liveness check completed!");
    // Navigate to next step
  };

  return (
    <QueryClientProvider client={queryClient}>
      <LivenessDetector
        config={config}
        token={localStorage.getItem("authToken")}
        gotoNext={handleComplete}
      />
    </QueryClientProvider>
  );
}
```

### Advanced Example with Customization

```tsx
import React, { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
  LivenessDetector,
  type LivenessConfig,
} from "@finclusionaibuild/liveness-sdk";
import "@finclusionaibuild/liveness-sdk/styles.css";

const queryClient = new QueryClient();

function AdvancedLivenessExample() {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const config: LivenessConfig = {
    apiBaseUrl: process.env.REACT_APP_API_BASE_URL || "https://api.example.com",
    getAuthToken: () => {
      // Get token from your auth system
      return localStorage.getItem("authToken");
    },
    endpoints: {
      createSession: "/api/v1/liveliness/create",
      verify: "/api/v1/liveliness/{sessionId}/verify",
    },
  };

  const handleComplete = async () => {
    setIsLoading(true);
    try {
      // Handle successful liveness check
      console.log("Liveness verification successful!");
      // Proceed to next step in your flow
      window.location.href = "/next-step";
    } catch (err) {
      setError("Failed to process liveness result");
      console.error(err);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <QueryClientProvider client={queryClient}>
      <div className="container mx-auto p-4">
        {error && (
          <div className="mb-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded">
            {error}
          </div>
        )}

        <LivenessDetector
          config={config}
          token={localStorage.getItem("authToken")}
          gotoNext={handleComplete}
          logoUrl="/custom-logo.svg" // Optional: Custom logo
          instructionText="Please position your face in the center of the frame and follow the instructions."
        />
      </div>
    </QueryClientProvider>
  );
}
```

### Error Handling Example

```tsx
import React, { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { LivenessDetector, type LivenessError } from "@finclusionaibuild/liveness-sdk";
import "@finclusionaibuild/liveness-sdk/styles.css";

const queryClient = new QueryClient();

function ErrorHandlingExample() {
  const [error, setError] = useState<LivenessError | null>(null);
  const [retryCount, setRetryCount] = useState(0);

  const config = {
    apiBaseUrl: "https://api.example.com",
    getAuthToken: () => localStorage.getItem("authToken"),
    endpoints: {
      createSession: "/api/liveliness",
      verify: "/api/liveliness/{sessionId}",
    },
  };

  const handleComplete = () => {
    console.log("Success!");
    setError(null);
    setRetryCount(0);
  };

  const handleError = (err: LivenessError) => {
    setError(err);
    console.error("Liveness error:", err);

    // Handle specific error types
    if (err.response?.status === 403) {
      // Handle unauthorized
      alert("Session expired. Please login again.");
    } else if (err.response?.status === 400) {
      // Handle bad request
      if (retryCount < 3) {
        setRetryCount((prev) => prev + 1);
        // Component will retry automatically
      } else {
        alert("Maximum retry attempts reached. Please contact support.");
      }
    }
  };

  return (
    <QueryClientProvider client={queryClient}>
      <div>
        {error && (
          <div className="mb-4 p-4 bg-yellow-100 border border-yellow-400 rounded">
            <p className="font-semibold">Error occurred:</p>
            <p>{error.message || "Unknown error"}</p>
            <p className="text-sm mt-2">Retry count: {retryCount}/3</p>
          </div>
        )}

        <LivenessDetector
          config={config}
          token={localStorage.getItem("authToken")}
          gotoNext={handleComplete}
        />
      </div>
    </QueryClientProvider>
  );
}
```

## Component API

### LivenessDetector

Main liveness detection component with full verification flow.

**Props:**

```typescript
interface LivenessDetectorProps {
  config: LivenessConfig; // Required: Configuration object
  token?: string | null; // Optional: Authentication token
  gotoNext: () => void; // Required: Callback on success
  logoUrl?: string; // Optional: Custom logo URL (defaults to iDCERTIFY logo)
  instructionText?: string; // Optional: Custom instruction text
}
```

**Example:**

```tsx
<LivenessDetector
  config={config}
  token={authToken}
  gotoNext={() => console.log("Complete!")}
  logoUrl="/my-logo.svg"
  instructionText="Custom instructions here"
/>
```

### ExternalLiveness

Standalone liveness component for external pages or separate flows.

**Props:**

```typescript
interface ExternalLivenessProps {
  config: LivenessConfig;
  token?: string | null;
  gotoNext: () => void;
  region?: string; // Optional:
  logoUrl?: string;
  instructionText?: string;
}
```

**Example:**

```tsx
<ExternalLiveness
  config={config}
  token={authToken}
  gotoNext={handleComplete}
  region="us-west-2"
/>
```

### PaymentLiveness

Payment verification variant that compares face with user profile.

**Props:**

```typescript
interface PaymentLivenessProps {
  config: LivenessConfig;
  token?: string | null;
  gotoNext: () => void;
  handleClose?: () => void; // Optional: Close handler
  logoUrl?: string;
  instructionText?: string;
  onSendOtp?: () => Promise<void>; // Optional: OTP sending callback
}
```

**Example:**

```tsx
<PaymentLiveness
  config={config}
  token={authToken}
  gotoNext={handleComplete}
  handleClose={() => setShowModal(false)}
  onSendOtp={async () => {
    await sendOtpToUser();
  }}
/>
```

## Configuration

### LivenessConfig

```typescript
interface LivenessConfig {
  apiBaseUrl: string; // Base URL for your API
  getAuthToken: () => string | null; // Function to get auth token
  includeLivenessGuide?: boolean; // Optional: Show guide modal
  endpoints?: {
    createSession?: string; // Optional: Custom endpoint (default: '/api/liveliness')
    verify?: string; // Optional: Custom verify endpoint
    compareWithUser?: string; // Optional: Custom compare endpoint (PaymentLiveness)
  };
}
```

**Example:**

```tsx
const config: LivenessConfig = {
  apiBaseUrl: "https://api.example.com",
  getAuthToken: () => {
    // Your token retrieval logic
    return localStorage.getItem("token");
  },
  includeLivenessGuide: true, // Show pre-liveness guide modal
  endpoints: {
    createSession: "/custom/liveliness/create",
    verify: "/custom/liveliness/{sessionId}/verify",
  },
};
```

## React Native Usage (WebView)

To use this SDK in React Native applications, you'll need to embed it in a WebView component. The SDK provides a UMD build specifically for WebView compatibility. Here's a complete example:

### Installation

```bash
npm install react-native-webview
# For iOS, you may also need:
cd ios && pod install
```

### Complete React Native Example

```tsx
import React, { useRef, useState } from "react";
import { View, StyleSheet, Alert, Platform } from "react-native";
import { WebView } from "react-native-webview";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

// HTML template with SDK embedded
const getHTML = (token: string, apiBaseUrl: string) => `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <title>Liveness Check</title>
  <style>
    body {
      margin: 0;
      padding: 0;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
      overflow: hidden;
    }
    #root {
      width: 100vw;
      height: 100vh;
    }
  </style>
</head>
<body>
  <div id="root"></div>
  
  <!-- React and ReactDOM -->
  <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
  
  <!-- React Query -->
  <script src="https://unpkg.com/@tanstack/react-query@5/build/umd/index.production.min.js"></script>
  
  <!-- SDK CSS -->
  <link rel="stylesheet" href="https://unpkg.com/@finclusionaibuild/liveness-sdk@1.0.3/dist/styles.css">
  
  <!-- SDK Bundle (UMD) -->
  <script src="https://unpkg.com/@finclusionaibuild/liveness-sdk@1.0.3/dist/index.umd.js"></script>
  
  <script>
    // Access SDK from global window object (UMD build exposes FinclusionLivenessSDK)
    const { QueryClient, QueryClientProvider } = TanStackReactQuery;
    const { LivenessDetector } = window.FinclusionLivenessSDK;
    const { createRoot } = ReactDOM;
    
    const queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          retry: 2,
          refetchOnWindowFocus: false,
        },
      },
    });
    
    const config = {
      apiBaseUrl: '${apiBaseUrl}',
      getAuthToken: () => '${token}',
      endpoints: {
        createSession: '/api/liveliness',
        verify: '/api/liveliness/{sessionId}',
      },
    };
    
    const handleComplete = () => {
      // Send message to React Native
      window.ReactNativeWebView.postMessage(JSON.stringify({
        type: 'LIVENESS_COMPLETE',
        success: true,
      }));
    };
    
    const App = () => {
      return React.createElement(QueryClientProvider, { client: queryClient },
        React.createElement(LivenessDetector, {
          config: config,
          token: '${token}',
          gotoNext: handleComplete,
        })
      );
    };
    
    const root = createRoot(document.getElementById('root'));
    root.render(React.createElement(App));
  </script>
</body>
</html>
`;

function LivenessScreen() {
  const webViewRef = useRef<WebView>(null);
  const [loading, setLoading] = useState(true);

  const authToken = "your-auth-token-here"; // Get from your auth system
  const apiBaseUrl = "https://api.example.com";

  const handleMessage = (event: any) => {
    try {
      const data = JSON.parse(event.nativeEvent.data);

      switch (data.type) {
        case "LIVENESS_COMPLETE":
          Alert.alert("Success", "Liveness check completed!", [
            {
              text: "OK",
              onPress: () => {
                // Navigate to next screen
                navigation.navigate("NextScreen");
              },
            },
          ]);
          break;
        case "LIVENESS_ERROR":
          Alert.alert("Error", data.message || "Liveness check failed");
          break;
        default:
          console.log("WebView message:", data);
      }
    } catch (error) {
      console.error("Error parsing WebView message:", error);
    }
  };

  return (
    <View style={styles.container}>
      <WebView
        ref={webViewRef}
        source={{ html: getHTML(authToken, apiBaseUrl) }}
        onMessage={handleMessage}
        onLoadEnd={() => setLoading(false)}
        style={styles.webview}
        javaScriptEnabled={true}
        domStorageEnabled={true}
        mediaPlaybackRequiresUserAction={false}
        allowsInlineMediaPlayback={true}
        // Camera permissions
        permissions={["camera", "microphone"]}
        // iOS specific
        allowsBackForwardNavigationGestures={false}
        // Android specific
        mixedContentMode="always"
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  webview: {
    flex: 1,
  },
});

export default LivenessScreen;
```

### React Native with Local Bundle (Recommended)

For better performance and offline support, bundle the SDK locally:

```tsx
import React, { useRef } from "react";
import { View, StyleSheet } from "react-native";
import { WebView } from "react-native-webview";
import { Asset } from "expo-asset"; // or react-native-fs for bare React Native

function LivenessScreen() {
  const webViewRef = useRef<WebView>(null);

  // Load SDK bundle from local assets
  const [sdkHtml, setSdkHtml] = useState("");

  useEffect(() => {
    // Load your bundled HTML file
    // This should include React, React Query, and the SDK bundle
    loadSDKHTML().then(setSdkHtml);
  }, []);

  return (
    <View style={styles.container}>
      <WebView
        ref={webViewRef}
        source={{ html: sdkHtml }}
        onMessage={handleMessage}
        style={styles.webview}
        javaScriptEnabled={true}
        domStorageEnabled={true}
        mediaPlaybackRequiresUserAction={false}
        allowsInlineMediaPlayback={true}
      />
    </View>
  );
}
```

### React Native Permissions Setup

#### iOS (Info.plist)

Add camera and microphone permissions:

```xml
<key>NSCameraUsageDescription</key>
<string>We need access to your camera for face verification</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need access to your microphone for face verification</string>
```

#### Android (AndroidManifest.xml)

Add camera and internet permissions:

```xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="android.hardware.camera" android:required="true" />
<uses-feature android:name="android.hardware.camera.front" android:required="true" />
```

### React Native Error Handling

```tsx
const handleMessage = (event: any) => {
  try {
    const data = JSON.parse(event.nativeEvent.data);

    if (data.type === "LIVENESS_ERROR") {
      const error = data.error;

      if (error.response?.status === 403) {
        // Handle unauthorized - redirect to login
        navigation.navigate("Login");
      } else if (error.response?.status === 400) {
        // Handle bad request
        Alert.alert("Verification Failed", "Please try again");
      } else {
        // Generic error
        Alert.alert("Error", error.message || "Something went wrong");
      }
    }
  } catch (error) {
    console.error("Error handling message:", error);
  }
};

// Handle WebView errors
const handleError = (syntheticEvent: any) => {
  const { nativeEvent } = syntheticEvent;
  console.error("WebView error: ", nativeEvent);
  Alert.alert("Error", "Failed to load liveness check");
};
```

## TypeScript Types

The SDK exports comprehensive TypeScript types:

```typescript
import type {
  LivenessConfig,
  LivenessSession,
  LivenessVerificationResponse,
  FaceMatchResponse,
  LivenessError,
} from "@finclusionaibuild/liveness-sdk";

// Example usage
const config: LivenessConfig = {
  apiBaseUrl: "https://api.example.com",
  getAuthToken: () => localStorage.getItem("token"),
};

const handleResponse = (response: LivenessVerificationResponse) => {
  if (response.confidence > 60) {
    console.log("Verification successful!", response.confidence);
  }
};
```

## API Endpoints

The SDK expects your backend to provide these endpoints:

### Create Session

**Endpoint:** `GET /api/liveliness` (or custom via `config.endpoints.createSession`)

**Response:**

```json
{
  "sessionId": "string",
  "AccessKeyId": "string",
  "SecretAccessKey": "string",
  "SessionToken": "string",
  "Region": "string"
}
```

### Verify Session

**Endpoint:** `GET /api/liveliness/{sessionId}` (or custom via `config.endpoints.verify`)

**Response:**

```json
{
  "confidence": 85.5,
  "image": "base64-encoded-image",
  "success": true,
  "message": "Verification successful"
}
```

### Compare with User (PaymentLiveness)

**Endpoint:** `GET /api/liveliness/{sessionId}/compare-with-user` (or custom via `config.endpoints.compareWithUser`)

**Response:**

```json
{
  "isMatch": true,
  "confidence": 92.3,
  "details": "Face matches user profile"
}
```

## Styling

The SDK includes all necessary styles bundled. Simply import:

```tsx
import "@finclusionaibuild/liveness-sdk/styles.css";
```

The stylesheet includes:

- Tailwind CSS utilities (responsive, mobile-optimized)
- Finclusion UI styles (processed and rebranded)
- Component-specific styling

**Note:** The SDK is mobile-responsive by default. No additional CSS configuration needed.

## Troubleshooting

### Camera Not Working

**Web:**

- Ensure HTTPS (required for camera access)
- Check browser permissions
- Verify `getUserMedia` API is available

**React Native:**

- Check camera permissions in `Info.plist` (iOS) and `AndroidManifest.xml` (Android)
- Ensure WebView has `allowsInlineMediaPlayback={true}` (iOS)
- Verify `mediaPlaybackRequiresUserAction={false}`

### Styles Not Applying

- Ensure you've imported the CSS: `import '@finclusionaibuild/liveness-sdk/styles.css';`
- Check that the CSS file is being loaded (inspect network tab)
- Verify no CSS conflicts with your app's styles

### React Query Errors

- Ensure `QueryClientProvider` wraps your component
- Verify React Query version matches peer dependency (`^5.39.0`)
- Check that `QueryClient` is created outside component render

### API Errors

- Verify `apiBaseUrl` is correct
- Check authentication token is valid
- Ensure endpoints match your backend API
- Check CORS settings if using cross-origin requests

### Build Errors

- Ensure all peer dependencies are installed
- Verify Node.js version (recommended: 18+)
- Check TypeScript version compatibility

## Examples

### Modal Integration

```tsx
import React, { useState } from "react";
import { LivenessDetector } from "@finclusionaibuild/liveness-sdk";

function LivenessModal() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <button onClick={() => setIsOpen(true)}>Start Verification</button>

      {isOpen && (
        <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
          <div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-auto p-4">
            <div className="flex justify-between items-center mb-4">
              <h2 className="text-xl font-bold">Face Verification</h2>
              <button onClick={() => setIsOpen(false)}>✕</button>
            </div>

            <LivenessDetector
              config={config}
              token={token}
              gotoNext={() => {
                setIsOpen(false);
                // Handle success
              }}
            />
          </div>
        </div>
      )}
    </>
  );
}
```

### With Loading States

```tsx
import React, { useState } from "react";
import { LivenessDetector } from "@finclusionaibuild/liveness-sdk";

function LivenessWithLoading() {
  const [isVerifying, setIsVerifying] = useState(false);

  return (
    <div>
      {isVerifying && (
        <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
          <div className="bg-white p-6 rounded-lg">
            <p>Processing verification...</p>
          </div>
        </div>
      )}

      <LivenessDetector
        config={config}
        token={token}
        gotoNext={async () => {
          setIsVerifying(true);
          try {
            // Process result
            await processVerification();
          } finally {
            setIsVerifying(false);
          }
        }}
      />
    </div>
  );
}
```

## License

MIT

## Support

For issues, questions, or contributions, please visit our [GitHub repository](https://github.com/finclusion/liveness-sdk).

## Version

Current version: **1.0.3**