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
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:
- Authenticate the user - Verify the user's identity using your authentication system (session, JWT, OAuth, etc.)
- Validate the email - Ensure a valid email address is provided
- Create the verification session - Use the Zyphe SDK to generate a verification session
- Return the verification URL - Send the URL back to the mobile app
- Handle errors appropriately - Manage authentication failures and session creation errors
Install the SDK
- pnpm
- npm
- Yarn
- Bun
pnpm add @zyphe-sdk/core
npm install --save @zyphe-sdk/core
yarn add @zyphe-sdk/core
bun 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
}
}
]
]
}
}
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
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:
- Use your computer's local network IP address
- Ensure both devices are on the same WiFi network
- 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
usesCleartextTrafficfrom 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 setmediaPlaybackRequiresUserAction={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