React Native Walkthrough
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.7.0
and >=0.76.6
. - Kotlin Version >=
1.9.0
and <2.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";
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}
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: use when mounting/unmounting multiple instances. ( unnecessary if your flow dismounts the component on its own )
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 handleStartVerification = async () => {
try {
if (thumbnail) {
/**
* Clears the image taken if exists.
* Clears the prediction text.
* Calls retake() to start a fresh attempt under the same referenceId.
*/
setThumbnail(null);
guidanceInputRef.current?.setNativeProps({ text: "" });
await retake();
}
// Begin camera preview and analysis
setStartVerification(true);
} catch (error) {
console.log(error);
}
};
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 handleStopVerification = async () => {
if (startVerification) {
// Stop analysis
setStartVerification(false);
// Reset flash and zoom to defaults
setIsFlashOn(false);
setIsZoomedIn(false);
// ends the attempt and emits a camera decided event.
return await endAttempt();
}
};
New Session
If you need to start a completely new session (new referenceId), clear all UI state, call endAttempt(), then generate a new referenceId and begin verification again.
const newSession = async () => {
setThumbnail(null);
setIsFlashOn(false);
setIsZoomedIn(false);
guidanceInputRef.current?.setNativeProps({ text: "" });
await endAttempt();
setReferenceId(Date.now().toString());
setStartVerification(true);
};
import React, { useState, useEffect, useRef } from "react";
import { View, StyleSheet, Button, Image, TextInput } from "react-native";
import {
setDelay,
setTimeout,
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 initializeCamera() {
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);
}
export default function App() {
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 | null>(null);
const guidanceInputRef = useRef<TextInput>(null);
useEffect(() => {
let unsubscriber: (() => void) | undefined;
(async () => {
await initializeCamera();
unsubscriber = subscribeToEvents({
capturDidGenerateEvent: (state, metadata) => {
if (state === "cameraDecided") {
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");
},
});
})();
return () => {
unsubscriber?.();
};
}, []);
useEffect(() => {
setReferenceId(Date.now().toString());
}, []);
const handleStartVerification = async () => {
try {
if (thumbnail) {
/**
* Clears the image taken if exists.
* Clears the prediction text
* Start a new attempt ( thumbnail == true means that this is a subsequent attempt )
* Retake has to be called if it's a subsequent attempt.
*/
setThumbnail(null);
guidanceInputRef.current?.setNativeProps({
text: "",
});
await retake();
}
/**
* Starts the camera, if it's not a retake there's no need to call retake and just start immediately.
*/
setStartVerification(true);
} catch (error) {
console.log(error);
}
};
const handleStopVerificiation = async () => {
if (startVerification) {
setStartVerification(false);
setIsFlashOn(false);
setIsZoomedIn(false);
return await endAttempt();
}
};
const handleFlash = () => {
setIsFlashOn((prev) => !prev);
};
const handleZoom = () => {
setIsZoomedIn((prev) => !prev);
};
const newSession = async () => {
/**
* Resets thumbnail, flash, zoom and prediction text.
* Ends the attempt
* Then starts the verification
* This will start a new session with a new reference.
*/
setThumbnail(null);
setIsFlashOn(false);
setIsZoomedIn(false);
guidanceInputRef.current?.setNativeProps({
text: "",
});
await endAttempt();
setReferenceId(Date.now().toString());
setStartVerification(true);
};
return (
<View style={styles.container}>
<View style={styles.cameraContainer}>
<CapturCameraView
key={referenceId}
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={"Start"} onPress={handleStartVerification} />
<Button title={"Stop"} onPress={handleStopVerificiation} />
<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 about 1 month ago