Skip to main content

React Native

Learn how to integrate Unforgettable SDK into your React Native application.

Installation

npm install @rarimo/unforgettable-sdk

Additional Dependencies

For WebView integration, install:

npm install react-native-webview

For iOS, install pods:

cd ios && pod install && cd ..

Quick Start

Here's a minimal example to get started:

import React, { useEffect, useState } from 'react'
import { View, ActivityIndicator } from 'react-native'
import { WebView } from 'react-native-webview'
import { UnforgettableSdk, RecoveryFactor, NotFoundError } from '@rarimo/unforgettable-sdk'

export const RecoveryScreen = () => {
const [recoveryUrl, setRecoveryUrl] = useState<string | null>(null)
const [sdk, setSdk] = useState<UnforgettableSdk | null>(null)

useEffect(() => {
initRecovery()
}, [])

const initRecovery = async () => {
// 1. Initialize SDK
const newSdk = new UnforgettableSdk({
mode: 'create',
factors: [RecoveryFactor.Face, RecoveryFactor.Image],
})

// 2. Get recovery URL
const url = await newSdk.getRecoveryUrl()
setRecoveryUrl(url)
setSdk(newSdk)

// 3. Poll for key
pollForKey(newSdk)
}

const pollForKey = async (sdkInstance: UnforgettableSdk) => {
let attempts = 0
while (attempts < 60) {
try {
const key = await sdkInstance.getRecoveredKey()
console.log('✅ Recovery successful:', key)
return
} catch (error) {
if (error instanceof NotFoundError) {
attempts++
await new Promise(resolve => setTimeout(resolve, 3000))
}
}
}
}

if (!recoveryUrl) return <ActivityIndicator />

return (
<View style={{ flex: 1 }}>
<WebView
source={{ uri: recoveryUrl }}
javaScriptEnabled={true}
domStorageEnabled={true}
/>
</View>
)
}

Basic Setup

Import the SDK

import { UnforgettableSdk, RecoveryFactor } from '@rarimo/unforgettable-sdk'

Initialize the SDK

const sdk = new UnforgettableSdk({
mode: 'create', // or 'restore'
factors: [RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password],
walletAddress: '0x...', // Required for 'restore' mode
group: 'my-mobile-app',
})

WebView Component

Create a reusable WebView component for recovery:

import React, { useEffect, useState } from 'react'
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native'
import { WebView } from 'react-native-webview'
import { UnforgettableSdk, RecoveryFactor, NotFoundError } from '@rarimo/unforgettable-sdk'

interface RecoveryWebViewProps {
mode: 'create' | 'restore'
walletAddress?: string
onSuccess: (privateKey: string) => void
onError: (error: Error) => void
}

export const RecoveryWebView: React.FC<RecoveryWebViewProps> = ({
mode,
walletAddress,
onSuccess,
onError,
}) => {
const [recoveryUrl, setRecoveryUrl] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [sdk, setSdk] = useState<UnforgettableSdk | null>(null)

useEffect(() => {
initializeSDK()
}, [])

const initializeSDK = async () => {
try {
const newSdk = new UnforgettableSdk({
mode,
factors: [RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password],
walletAddress,
group: 'react-native-app',
})

const url = await newSdk.getRecoveryUrl()
setRecoveryUrl(url)
setSdk(newSdk)
setLoading(false)

// Start polling
startPolling(newSdk)
} catch (error) {
setLoading(false)
onError(error as Error)
}
}

const startPolling = async (sdkInstance: UnforgettableSdk) => {
const maxAttempts = 60
let attempts = 0

const poll = async () => {
try {
const privateKey = await sdkInstance.getRecoveredKey()
onSuccess(privateKey)
} catch (error) {
if (error instanceof NotFoundError && attempts < maxAttempts) {
attempts++
setTimeout(poll, 3000)
} else if (!(error instanceof NotFoundError)) {
onError(error as Error)
}
}
}

poll()
}

if (loading) {
return (
<View style={styles.container}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={styles.loadingText}>Loading recovery...</Text>
</View>
)
}

if (!recoveryUrl) {
return (
<View style={styles.container}>
<Text style={styles.errorText}>Failed to initialize recovery</Text>
</View>
)
}

return (
<View style={styles.container}>
<WebView
source={{ uri: recoveryUrl }}
style={styles.webview}
javaScriptEnabled={true}
domStorageEnabled={true}
mediaPlaybackRequiresUserAction={false}
allowsInlineMediaPlayback={true}
/>
<View style={styles.statusContainer}>
<ActivityIndicator size="small" color="#007AFF" />
<Text style={styles.statusText}>Waiting for completion...</Text>
</View>
</View>
)
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
webview: {
flex: 1,
width: '100%',
},
statusContainer: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
},
statusText: {
marginLeft: 8,
fontSize: 14,
color: '#666',
},
loadingText: {
marginTop: 16,
fontSize: 16,
color: '#666',
},
errorText: {
fontSize: 16,
color: '#ff0000',
},
})

Complete Example

Create Recovery Screen

import React, { useState } from 'react'
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Alert,
SafeAreaView,
} from 'react-native'
import { RecoveryWebView } from './components/RecoveryWebView'

export const CreateRecoveryScreen = () => {
const [showQR, setShowQR] = useState(false)
const [privateKey, setPrivateKey] = useState<string | null>(null)

const handleStartRecovery = () => {
setShowQR(true)
}

const handleSuccess = (key: string) => {
setPrivateKey(key)
Alert.alert('Success', 'Recovery set up successfully!')
// Create wallet with the key
createWallet(key)
}

const handleError = (error: Error) => {
Alert.alert('Error', error.message)
setShowQR(false)
}

const createWallet = (key: string) => {
// Your wallet creation logic
console.log('Creating wallet with key:', key)
}

return (
<SafeAreaView style={styles.container}>
<View style={styles.content}>
<Text style={styles.title}>Set Up Account Recovery</Text>

{!showQR && !privateKey && (
<>
<Text style={styles.description}>
Protect your account with biometric recovery.
You can restore access using your face or other factors.
</Text>
<TouchableOpacity
style={styles.button}
onPress={handleStartRecovery}
>
<Text style={styles.buttonText}>Start Setup</Text>
</TouchableOpacity>
</>
)}

{showQR && !privateKey && (
<RecoveryWebView
mode="create"
onSuccess={handleSuccess}
onError={handleError}
/>
)}

{privateKey && (
<View style={styles.successContainer}>
<Text style={styles.successIcon}></Text>
<Text style={styles.successText}>
Recovery Set Up Successfully!
</Text>
<Text style={styles.successDescription}>
Your account is now protected.
</Text>
</View>
)}
</View>
</SafeAreaView>
)
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
content: {
flex: 1,
padding: 20,
justifyContent: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 16,
},
description: {
fontSize: 16,
color: '#666',
textAlign: 'center',
marginBottom: 32,
lineHeight: 24,
},
button: {
backgroundColor: '#007AFF',
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
buttonText: {
color: '#fff',
fontSize: 18,
fontWeight: '600',
},
successContainer: {
alignItems: 'center',
},
successIcon: {
fontSize: 64,
marginBottom: 16,
},
successText: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 8,
},
successDescription: {
fontSize: 16,
color: '#666',
},
})

Restore Account Screen

import React, { useState } from 'react'
import {
View,
Text,
TextInput,
StyleSheet,
TouchableOpacity,
Alert,
SafeAreaView,
KeyboardAvoidingView,
Platform,
} from 'react-native'
import { RecoveryWebView } from './components/RecoveryWebView'

export const RestoreAccountScreen = () => {
const [walletAddress, setWalletAddress] = useState('')
const [showQR, setShowQR] = useState(false)
const [recovered, setRecovered] = useState(false)

const handleRestore = () => {
if (!walletAddress.trim()) {
Alert.alert('Error', 'Please enter a wallet address')
return
}
setShowQR(true)
}

const handleSuccess = (key: string) => {
setRecovered(true)
Alert.alert('Success', 'Account restored successfully!')
// Restore wallet access
restoreWallet(key)
}

const handleError = (error: Error) => {
Alert.alert('Error', error.message)
setShowQR(false)
}

const restoreWallet = (key: string) => {
console.log('Restoring wallet with key:', key)
// Import wallet, restore user access, etc.
}

return (
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardView}
>
<View style={styles.content}>
<Text style={styles.title}>Restore Your Account</Text>

{!showQR && !recovered && (
<>
<Text style={styles.description}>
Enter your wallet address to restore access
</Text>
<TextInput
style={styles.input}
placeholder="0x..."
value={walletAddress}
onChangeText={setWalletAddress}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity
style={styles.button}
onPress={handleRestore}
>
<Text style={styles.buttonText}>Restore Account</Text>
</TouchableOpacity>
</>
)}

{showQR && !recovered && (
<>
<RecoveryWebView
mode="restore"
walletAddress={walletAddress}
onSuccess={handleSuccess}
onError={handleError}
/>
<TouchableOpacity
style={styles.cancelButton}
onPress={() => setShowQR(false)}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
</>
)}

{recovered && (
<View style={styles.successContainer}>
<Text style={styles.successIcon}></Text>
<Text style={styles.successText}>Account Restored!</Text>
<Text style={styles.successDescription}>Welcome back!</Text>
</View>
)}
</View>
</KeyboardAvoidingView>
</SafeAreaView>
)
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
keyboardView: {
flex: 1,
},
content: {
flex: 1,
padding: 20,
justifyContent: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 16,
},
description: {
fontSize: 16,
color: '#666',
textAlign: 'center',
marginBottom: 24,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 16,
fontSize: 16,
marginBottom: 16,
},
button: {
backgroundColor: '#007AFF',
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
buttonText: {
color: '#fff',
fontSize: 18,
fontWeight: '600',
},
cancelButton: {
marginTop: 16,
padding: 16,
alignItems: 'center',
},
cancelButtonText: {
color: '#007AFF',
fontSize: 16,
},
successContainer: {
alignItems: 'center',
},
successIcon: {
fontSize: 64,
marginBottom: 16,
},
successText: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 8,
},
successDescription: {
fontSize: 16,
color: '#666',
},
})

Deep Linking

To handle deep links from the Unforgettable app back to your app:

iOS Configuration

Add to ios/YourApp/Info.plist:

<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>yourapp</string>
</array>
</dict>
</array>

Android Configuration

Add to android/app/src/main/AndroidManifest.xml:

<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="yourapp" />
</intent-filter>
import { useEffect } from 'react'
import { Linking } from 'react-native'

function App() {
useEffect(() => {
// Handle initial URL if app was opened from a deep link
Linking.getInitialURL().then(url => {
if (url) handleDeepLink(url)
})

// Listen for deep links while app is running
const subscription = Linking.addEventListener('url', ({ url }) => {
handleDeepLink(url)
})

return () => subscription.remove()
}, [])

const handleDeepLink = (url: string) => {
// Parse and handle the deep link
console.log('Deep link received:', url)
}

return <App />
}

Platform-Specific Considerations

iOS

For iOS, add camera permissions to ios/YourApp/Info.plist:

<key>NSCameraUsageDescription</key>
<string>Camera access is required for face verification during account recovery</string>

Android

Add permissions to AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />

Error Handling

import { NotFoundError } from '@rarimo/unforgettable-sdk'

try {
const key = await sdk.getRecoveredKey()
// Success
} catch (error) {
if (error instanceof NotFoundError) {
console.log('Data not ready yet')
} else {
console.error('Unexpected error:', error)
Alert.alert('Error', 'Something went wrong')
}
}

TypeScript Support

Full TypeScript support included:

import type {
UnforgettableSdkOptions,
RecoveredData,
RecoveryFactor,
} from '@rarimo/unforgettable-sdk'

const options: UnforgettableSdkOptions = {
mode: 'create',
factors: [RecoveryFactor.Face],
}

Next Steps