Skip to main content

React Native

This guide shows you how to integrate Zyphe verification into a React Native application using Expo or bare React Native.

Overview

The React Native integration uses a WebView to display the Zyphe verification flow. You'll need:

  • A backend server to create verification sessions securely
  • A React Native app with WebView to display the verification flow
  • Proper permissions configured for camera, microphone, and other required features

Prerequisites

  • Node.js 18+ installed
  • React Native or Expo project set up
  • Zyphe account with API credentials (Get started here)

Backend Setup

Security

Never expose your API key in the mobile app. Always create verification sessions on your backend server.

You need to create a backend endpoint that generates verification session URLs securely. You can use any backend framework (Express, Fastify, NestJS, Hono, etc.).

Your backend service should:

  1. Authenticate the user - Verify the user's identity using your authentication system (session, JWT, OAuth, etc.)
  2. Validate the email - Ensure a valid email address is provided
  3. Create the verification session - Use the Zyphe SDK to generate a verification session
  4. Return the verification URL - Send the URL back to the mobile app
  5. Handle errors appropriately - Manage authentication failures and session creation errors

Install the SDK

pnpm add @zyphe-sdk/core

Create Verification Session Endpoint

Create an API endpoint that receives the user's email and returns a verification URL:

import {
createVerificationSession,
constructVerificationSessionUrl,
type SDKOptions,
type InitializeZypheFlowParams,
} from '@zyphe-sdk/core'

const PUBLISHABLE_API_KEY = 'your-api-key' // Your API Key
const FLOW_ID = 'your-flow-id' // The flow ID you want to use for creating the verification session
const FLOW_STEP_ID = 'your-flow-step-id' // The flow step ID you want to use for creating the verification session (typically the first step)

// Your API endpoint handler (adapt to your framework)
async function createVerificationSessionHandler(email: string) {
const opts: SDKOptions = {
apiKey: PUBLISHABLE_API_KEY,
environment: 'production',
}

const flowParams: InitializeZypheFlowParams = {
email,
flowId: FLOW_ID,
flowStepId: FLOW_STEP_ID,
isSandbox: true,
product: 'kyc',
}

const { error, data: verificationSession } = await createVerificationSession(flowParams, opts)

if (error) {
throw new Error('Failed to create verification session')
}

const verificationSessionUrl = constructVerificationSessionUrl({
opts,
verificationSession,
flowParams,
})

return {
url: verificationSessionUrl,
sessionId: verificationSession.id,
}
}

Fullscreen Mode

By default, the verification UI displays as cards. To show the UI in fullscreen mode, add the zypheFullscreen=true query parameter to the verification URL:

const verificationSessionUrl = constructVerificationSessionUrl({
opts,
verificationSession,
flowParams,
})

// Add fullscreen mode
const fullscreenUrl = `${verificationSessionUrl}&zypheFullscreen=true`

This is particularly recommended for mobile WebView implementations to provide a better user experience.


React Native App Setup

Install Required Packages

npm install react-native-webview
# For Expo:
npx expo install react-native-webview expo-camera expo-constants

For bare React Native, follow the react-native-webview installation guide.


Platform Configuration

Android Permissions

Expo (Managed Workflow)

Add permissions to your app.json or app.config.js:

{
"expo": {
"android": {
"permissions": [
"android.permission.INTERNET",
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO",
"android.permission.MODIFY_AUDIO_SETTINGS"
]
},
"plugins": [
[
"expo-build-properties",
{
"android": {
"usesCleartextTraffic": true
}
}
]
]
}
}
warning

The usesCleartextTraffic setting is only needed for development to allow HTTP connections to localhost. Remove this for production builds and use HTTPS endpoints only.

Bare React Native

Edit android/app/src/main/AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required permissions for Zyphe verification -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="true" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

<application
android:usesCleartextTraffic="true">
<!-- Your app content -->
</application>
</manifest>

Android Permissions Explained

  • INTERNET: Required for all network communication
  • CAMERA: Required for document scanning and selfie verification
  • RECORD_AUDIO: Required for liveness detection and video recording
  • MODIFY_AUDIO_SETTINGS: Required for audio settings during verification

iOS Permissions

Expo (Managed Workflow)

Add permissions to your app.json or app.config.js:

{
"expo": {
"ios": {
"infoPlist": {
"NSCameraUsageDescription": "Camera access is required for identity verification and document scanning.",
"NSMicrophoneUsageDescription": "Microphone access is required for liveness detection during identity verification.",
"NSPhotoLibraryUsageDescription": "Photo library access allows you to upload existing photos for verification.",
"NSLocationWhenInUseUsageDescription": "Location access may be used for fraud prevention during verification."
}
}
}
}

Bare React Native

Edit ios/YourAppName/Info.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Required permissions for Zyphe verification -->
<key>NSCameraUsageDescription</key>
<string>Camera access is required for identity verification and document scanning.</string>

<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is required for liveness detection during identity verification.</string>

<key>NSPhotoLibraryUsageDescription</key>
<string>Photo library access allows you to upload existing photos for verification.</string>

<key>NSLocationWhenInUseUsageDescription</key>
<string>Location access may be used for fraud prevention during verification.</string>
</dict>
</plist>

iOS Permissions Explained

  • NSCameraUsageDescription: Required for document capture and selfie verification
  • NSMicrophoneUsageDescription: Required for liveness detection and video recording
  • NSPhotoLibraryUsageDescription: Allows users to upload existing photos
  • NSLocationWhenInUseUsageDescription: Optional but recommended for fraud detection
info

The description strings will be shown to users when iOS requests permission. Make them clear and specific to pass App Store review.


Implementation

Basic Example

Here's a complete React Native component that implements Zyphe verification:

import React, { useState } from 'react'
import { StyleSheet, Text, View, TextInput, TouchableOpacity, ActivityIndicator, Alert, SafeAreaView, Platform } from 'react-native'
import { WebView } from 'react-native-webview'
import { Camera } from 'expo-camera' // For Expo, or use PermissionsAndroid for bare RN

const BACKEND_URL = 'http://localhost:3000' // Your backend URL

export default function App() {
const [email, setEmail] = useState('')
const [verificationUrl, setVerificationUrl] = useState('')
const [loading, setLoading] = useState(false)
const [showWebView, setShowWebView] = useState(false)

const createVerificationSession = async () => {
if (!email) {
Alert.alert('Error', 'Please enter your email')
return
}

setLoading(true)
try {
// Request camera permission before proceeding
const { status } = await Camera.requestCameraPermissionsAsync()

if (status !== 'granted') {
Alert.alert(
'Camera Permission Required',
'Camera access is required for identity verification. Please enable camera permissions in your device settings.',
)
setLoading(false)
return
}

const response = await fetch(`${BACKEND_URL}/api/create-verification-session`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
})

const data = await response.json()

if (response.ok && data.url) {
setVerificationUrl(data.url)
setShowWebView(true)
} else {
Alert.alert('Error', data.error || 'Failed to create verification session')
}
} catch (error) {
Alert.alert('Error', 'Failed to connect to the server. Make sure the backend is running.')
} finally {
setLoading(false)
}
}

const handleWebViewMessage = (event: any) => {
// Handle messages from the WebView if needed
console.log('WebView message:', event.nativeEvent.data)
}

const resetSession = () => {
setShowWebView(false)
setVerificationUrl('')
setEmail('')
}

if (showWebView && verificationUrl) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.webViewHeader}>
<TouchableOpacity style={styles.backButton} onPress={resetSession}>
<Text style={styles.backButtonText}>← Back</Text>
</TouchableOpacity>
<Text style={styles.headerTitle}>Verification</Text>
</View>
<WebView
source={{ uri: verificationUrl }}
style={styles.webView}
allowsInlineMediaPlayback={true}
mediaPlaybackRequiresUserAction={false}
javaScriptEnabled={true}
domStorageEnabled={true}
startInLoadingState={true}
onMessage={handleWebViewMessage}
renderLoading={() => (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#007AFF" />
</View>
)}
/>
</SafeAreaView>
)
}

return (
<SafeAreaView style={styles.container}>
<View style={styles.content}>
<Text style={styles.title}>Zyphe Verification</Text>
<Text style={styles.subtitle}>Enter your email to start the verification process</Text>

<TextInput
style={styles.input}
placeholder="Enter your email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>

<TouchableOpacity style={[styles.button, loading && styles.buttonDisabled]} onPress={createVerificationSession} disabled={loading}>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.buttonText}>Start Verification</Text>}
</TouchableOpacity>

<View style={styles.infoContainer}>
<Text style={styles.infoText}>Make sure your backend server is running</Text>
</View>
</View>
</SafeAreaView>
)
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
content: {
flex: 1,
justifyContent: 'center',
padding: 20,
},
title: {
fontSize: 32,
fontWeight: 'bold',
marginBottom: 10,
textAlign: 'center',
color: '#333',
},
subtitle: {
fontSize: 16,
marginBottom: 30,
textAlign: 'center',
color: '#666',
},
input: {
backgroundColor: '#fff',
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 15,
fontSize: 16,
marginBottom: 20,
},
button: {
backgroundColor: '#007AFF',
borderRadius: 8,
padding: 15,
alignItems: 'center',
},
buttonDisabled: {
backgroundColor: '#9cc9ff',
},
buttonText: {
color: '#fff',
fontSize: 18,
fontWeight: '600',
},
infoContainer: {
marginTop: 30,
padding: 15,
backgroundColor: '#e8f4ff',
borderRadius: 8,
},
infoText: {
fontSize: 14,
color: '#007AFF',
textAlign: 'center',
},
webViewHeader: {
flexDirection: 'row',
alignItems: 'center',
padding: 15,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#ddd',
},
backButton: {
padding: 5,
},
backButtonText: {
fontSize: 18,
color: '#007AFF',
fontWeight: '600',
},
headerTitle: {
flex: 1,
fontSize: 18,
fontWeight: '600',
textAlign: 'center',
marginRight: 60,
color: '#333',
},
webView: {
flex: 1,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
})

WebView Configuration

The WebView must be configured with specific properties for the verification flow to work properly:

<WebView
source={{ uri: verificationUrl }}
allowsInlineMediaPlayback={true} // Required: Allows inline media playback
mediaPlaybackRequiresUserAction={false} // Required: Enables autoplay
javaScriptEnabled={true} // Required: Enables JavaScript
domStorageEnabled={true} // Required: For session management
startInLoadingState={true} // Recommended: Shows loading indicator
onMessage={handleWebViewMessage} // Optional: Handle messages from WebView
/>

Key Properties Explained

  • allowsInlineMediaPlayback: Allows the camera/media to play inline instead of fullscreen (iOS)
  • mediaPlaybackRequiresUserAction: Disables the requirement for user interaction before media plays
  • javaScriptEnabled: Required for the verification flow to function
  • domStorageEnabled: Required for storing session data and state

Permission Handling

Request Permissions at Runtime

It's recommended to request camera permissions before starting the verification flow:

Using Expo Camera

import { Camera } from 'expo-camera'

const requestPermissions = async () => {
const { status } = await Camera.requestCameraPermissionsAsync()

if (status !== 'granted') {
Alert.alert('Permission Required', 'Camera access is required for verification.')
return false
}

return true
}

Using Bare React Native (Android)

import { PermissionsAndroid, Platform } from 'react-native'

const requestAndroidPermissions = async () => {
if (Platform.OS !== 'android') return true

try {
const granted = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.CAMERA,
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
])

return (
granted['android.permission.CAMERA'] === PermissionsAndroid.RESULTS.GRANTED &&
granted['android.permission.RECORD_AUDIO'] === PermissionsAndroid.RESULTS.GRANTED
)
} catch (err) {
console.warn(err)
return false
}
}

Testing

iOS Simulator

Use the default localhost URL:

const BACKEND_URL = 'http://localhost:3000'

Android Emulator

Use the special Android emulator address:

const BACKEND_URL = 'http://10.0.2.2:3000' // Points to host machine's localhost

Physical Devices

For testing on physical devices, you'll need to:

  1. Use your computer's local network IP address
  2. Ensure both devices are on the same WiFi network
  3. Configure firewall to allow connections on port 3000
const BACKEND_URL = 'http://192.168.1.100:3000' // Replace with your local IP

Or use automatic IP detection with the ip package.


Production Checklist

Before deploying to production:

  • ✅ Remove usesCleartextTraffic from Android config
  • ✅ Use HTTPS endpoints for all API calls
  • ✅ Never expose API keys in the mobile app
  • ✅ Test on real devices with production builds
  • ✅ Verify all permission descriptions are clear and accurate
  • ✅ Handle permission denials gracefully
  • ✅ Add error handling for network failures
  • ✅ Test the complete verification flow end-to-end

Common Issues

Camera Not Working in WebView

Make sure:

  • All required permissions are granted
  • allowsInlineMediaPlayback={true} is set
  • mediaPlaybackRequiresUserAction={false} is set
  • Camera permissions are declared in AndroidManifest.xml/Info.plist

Backend Connection Errors

  • iOS Simulator: Use http://localhost:3000
  • Android Emulator: Use http://10.0.2.2:3000
  • Physical Devices: Use your computer's local network IP
  • Ensure backend server is running

Permissions Not Requested

  • Check that permission strings are in AndroidManifest.xml (Android) or Info.plist (iOS)
  • Verify you're calling the permission request methods
  • Check device settings to ensure permissions weren't permanently denied