React Native Walkthrough 0.6.6
React Native Captur Events SDK
Integrate Captur’s verification capabilities into your React Native apps. Below you’ll find installation steps, initialization guidance, event handling, and a full example screen.
Prerequisites
- A valid Captur API key (provided by the Captur team).
- React Native
0.74.*
-0.76.6
. - Kotlin Version
1.9.24
up until2.0.0
( 2.0.0+ not supported ) - iOS 15.4+
- Android 10+
Installation & Authentication
- Create or open your global npm config:
touch ~/.npmrc # create file if doesn't exist already
- Add your Access Token:
# ~/.npmrc
//registry.npmjs.org/:_authToken=ACCESS_TOKEN
- Install the Captur Events SDK:
# in your react-native project
npm install @captur-ai/captur-react-native-events
Platform setup
iOS Installation
- From the ios/ folder:
cd ios && pod install
- In Info.plist, add: ( or use XCode to add camera permissions )
<key>Privacy - Camera Usage Description</key>
<string>Your camera is used for verification</string>
Android Installation
- In
android/app/build.gradle
, add the AAR repository:
repositories {
google()
mavenCentral()
flatDir {
dirs '../../node_modules/@captur-ai/captur-react-native-events/android/libs'
}
}
- In the same file, include the Captur AAR and its dependencies:
dependencies {
...
implementation(name: 'capturMicroMobility-release', ext: 'aar') {
transitive = true
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
implementation("androidx.camera:camera-core:1.3.4")
implementation("androidx.camera:camera-camera2:1.3.4")
implementation("androidx.camera:camera-lifecycle:1.3.4")
implementation("androidx.camera:camera-view:1.3.4")
api("org.tensorflow:tensorflow-lite-task-vision:0.4.0")
implementation("com.squareup.retrofit2:adapter-rxjava3:2.9.0")
implementation("com.squareup.moshi:moshi-kotlin:1.14.0")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
}
...
}
- In android/app/src/main/AndroidManifest.xml, add:
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="android.hardware.camera.flash" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-permission android:name="android.permission.CAMERA" />
Initialization
- Set up your config and initialize the SDK before rendering any camera views:
const DELAY = 1;
const TIMEOUT = 25;
const LOCATION_NAME = "Toronto";
const ASSET_TYPE = "package"; // other values: eScooter, eBike...etc
const API_KEY = "captur-*******";
async function initializeCaptur() {
await setTimeout(TIMEOUT);
await setDelay(DELAY);
await setApiKey(API_KEY);
await prepareModel(LOCATION_NAME, ASSET_TYPE, 0.0, 0.0);
await getConfig(LOCATION_NAME, ASSET_TYPE, 0.0, 0.0);
}
Event Subscription
Use subscribeToEvents to receive both real-time guidance and final decision events from the Captur SDK. You register four callbacks:
- capturDidGenerateGuidance: fires continuously as the camera analyzes frames, providing user instructions (e.g. “move closer”, “rotate left”).
- capturDidRevokeGuidance: fires when the previous guidance no longer applies (e.g. user moved out of frame).
- capturDidGenerateEvent: fires on state changes, including the final "cameraDecided" state which delivers the compliant or non-compliant decision along with a base64 image.
- capturDidGenerateError: fires if any internal error occurs (e.g. network timeout, model load failure).
useEffect(() => {
let unsubscriber: (() => void) | undefined;
(async () => {
await initializeCaptur();
unsubscriber = subscribeToEvents({
capturDidGenerateGuidance: (guidanceMeta) => {
// guidanceMeta.guidanceTitle is a human-readable tip for the user
guidanceInputRef.current?.setNativeProps({
text: guidanceMeta.guidanceTitle,
});
},
capturDidRevokeGuidance: () => {
// clear or hide guidance when it’s no longer valid
guidanceInputRef.current?.setNativeProps({ text: "" });
},
capturDidGenerateEvent: (state, metadata) => {
// state === "cameraDecided" indicates the final result
if (state === "cameraDecided") {
// metadata.imageDataBase64 contains the decision image
setThumbnail("data:image/png;base64," + metadata?.imageDataBase64);
}
},
capturDidGenerateError: (error) => {
// handle errors (e.g. show a message or retry)
console.error("Captur error:", error);
},
});
})();
return () => {
unsubscriber?.();
};
}, []);
Camera Component
Render the camera preview and control flash/zoom:
<CapturCameraView
key={`${referenceId}-${startVerification}`} // This key is required to deal with surface attachment quirks.
style={styles.camera}
referenceId={referenceId ?? ''}
startVerification={startVerification}
isFlashOn={isFlashOn}
isZoomedIn={isZoomedIn}
/>
- referenceId: unique per session (e.g. rideId).
- startVerification: on/off toggle for preview & analysis.
- isFlashOn / isZoomedIn: control flash & zoom.
- key: The key helps with mounting and unmounting the camera preview and managing its life cycle.
Attempts & Sessions
Start an Attempt
Starting an attempt tells the Captur SDK to begin capturing camera frames and running its compliance model in real time. If you already have a thumbnail from a prior attempt in the same session, call retake() first to clear previous state—otherwise you can simply set startVerification to true.
const startCamera = async () => {
if (!capturInitialized) return;
if (!referenceId) return;
if (startVerification) return;
if (thumbnail) {
setThumbnail(null);
guidanceInputRef.current?.setNativeProps({
text: '',
});
retake();
}
setStartVerification(true);
};
End an Attempt
Ending an attempt stops the camera analysis, resets any toggles (flash, zoom), and emits a final cameraDecided event containing the compliance decision and a base64-encoded image.
const stopCamera = () => {
if (!capturInitialized) return;
if (!startVerification) return;
return endAttempt();
};
When you call endAttempt
an event called capturDidGenerateEvent
will be emitted from the SDK with a state with the value cameraDecided
. You should always perform the end attempt or capture photo logic inside the callback of that event. for example, you should reset the camera state and set the thumbnail/photo taken in the event listener like so:
subscribeToEvents({
capturDidGenerateEvent: (state, metadata) => {
if (state === 'cameraDecided') {
resetCameraState();
setThumbnail('data:image/png;base64,' + metadata?.imageDataBase64);
}
}
)
New Session
If you need to start a completely new session (new referenceId), clear all UI state, then generate a new referenceId and begin verification again.
const newSession = async () => {
setThumbnail(null);
resetCameraState();
guidanceInputRef.current?.setNativeProps({
text: '',
});
setReferenceId(Date.now().toString());
setStartVerification(true);
};
Full example
import { useState, useEffect, useRef } from 'react';
import { View, StyleSheet, Button, Image, TextInput } from 'react-native';
import {
setDelay,
setTimeout as cptrSetTimeout,
setApiKey,
prepareModel,
getConfig,
CapturCameraView,
subscribeToEvents,
endAttempt,
retake,
} from '@captur-ai/captur-react-native-events';
const DELAY = 1;
const TIMEOUT = 25;
const LOCATION_NAME = "Toronto";
const ASSET_TYPE = "package";
const API_KEY = "captur-***-****-**-**-**-****";
async function initializeCaptur() {
await cptrSetTimeout(TIMEOUT);
await setDelay(DELAY);
await setApiKey(API_KEY);
await prepareModel(LOCATION_NAME, ASSET_TYPE, 0.0, 0.0);
await getConfig(LOCATION_NAME, ASSET_TYPE, 0.0, 0.0);
}
export default function App() {
const [capturInitialized, setCapturInitialized] = useState(false);
const [startVerification, setStartVerification] = useState(false);
const [isFlashOn, setIsFlashOn] = useState(false);
const [isZoomedIn, setIsZoomedIn] = useState(false);
const [thumbnail, setThumbnail] = useState<string | null>(null);
const [referenceId, setReferenceId] = useState<string>(Date.now().toString());
const guidanceInputRef = useRef<TextInput>(null);
useEffect(() => {
let unsubscriber: (() => void) | undefined;
(async () => {
await initializeCaptur();
unsubscriber = subscribeToEvents({
capturDidGenerateEvent: (state, metadata) => {
if (state === 'cameraDecided') {
resetCameraState();
setThumbnail('data:image/png;base64,' + metadata?.imageDataBase64);
}
},
capturDidGenerateError: (err) => {
console.log('capturDidGenerateError', err);
},
capturDidGenerateGuidance: (meta) => {
guidanceInputRef.current?.setNativeProps({
text: meta.guidanceTitle,
});
},
capturDidRevokeGuidance: () => {
console.log('capturDidRevokeGuidance');
},
});
setCapturInitialized(true);
})();
return () => {
unsubscriber?.();
};
}, []);
const resetCameraState = () => {
setStartVerification(false);
setIsFlashOn(false);
setIsZoomedIn(false);
};
const startCamera = async () => {
if (!capturInitialized) return;
if (!referenceId) return;
if (startVerification) return;
if (thumbnail) {
setThumbnail(null);
guidanceInputRef.current?.setNativeProps({
text: '',
});
retake();
}
setStartVerification(true);
};
const stopCamera = () => {
if (!capturInitialized) return;
if (!startVerification) return;
return endAttempt();
};
const handleFlash = () => {
setIsFlashOn((prev) => !prev);
};
const handleZoom = () => {
setIsZoomedIn((prev) => !prev);
};
const newSession = async () => {
setThumbnail(null);
resetCameraState();
guidanceInputRef.current?.setNativeProps({
text: '',
});
setReferenceId(Date.now().toString());
setStartVerification(true);
};
return (
<View style={styles.container}>
<View style={styles.cameraContainer}>
<CapturCameraView
key={`${referenceId}-${startVerification}`}
style={styles.camera}
referenceId={referenceId ?? ''}
startVerification={startVerification}
isFlashOn={isFlashOn}
isZoomedIn={isZoomedIn}
/>
</View>
{thumbnail && (
<View style={styles.thumbnailContainer}>
<Image style={styles.thumbnail} source={{ uri: thumbnail }} />
</View>
)}
<View style={styles.actionButtons}>
<Button
title={startVerification ? 'Stop' : 'Start'}
onPress={startVerification ? stopCamera : startCamera}
/>
<Button
title={isFlashOn ? 'Turn flash off' : 'Turn flash on'}
onPress={handleFlash}
/>
<Button
title={isZoomedIn ? 'Zoom out' : 'Zoom in'}
onPress={handleZoom}
/>
<Button title={'New session'} onPress={newSession} />
</View>
<View style={styles.guidance}>
<TextInput
style={styles.guidanceText}
ref={guidanceInputRef}
editable={false}
/>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'flex-end',
},
cameraContainer: {
flex: 1,
position: 'relative',
justifyContent: 'flex-end',
},
camera: {
flex: 1,
},
thumbnailContainer: {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'black',
},
thumbnail: {
width: '100%',
height: '100%',
resizeMode: 'cover',
},
guidance: {
position: 'absolute',
top: '10%',
width: '100%',
zIndex: 14,
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.4)',
paddingVertical: 12,
},
guidanceText: {
color: 'white',
fontSize: 16,
},
actionButtons: {
position: 'absolute',
bottom: 0,
left: 0,
width: '100%',
zIndex: 20,
gap: 10,
},
});
Updated 5 days ago