Basic Usage
A step-by-step guide to integrating the camera UI with the usePetnowCamera hook and CameraView.
Before You Begin
Complete Getting Started before following this guide. You need the package installed, camera permissions configured, and a Petnow API key.
This guide walks you step by step through integrating pet nose-print/face capture into your app using the usePetnowCamera hook and <CameraView>.
Prepare the Camera
Create the controller with usePetnowCamera and wait for the ready state.
Display the Camera
Once ready, mount <CameraView> to show the preview and overlay.
Receive Events
Handle detection status, progress, and the finished result through callbacks.
Handle the Result
Upload the captured images to your app server.
Additional Features
Use retake, camera switching, and pause/resume.
Step 1: Prepare the Camera
usePetnowCamera is the hook that holds the license and returns a controller handle exposing the detection commands. It stores the apiKey in the native SDK (and stores it again when apiKey changes), and exposes the SDK readiness state through camera.state. Wait until status becomes 'ready' before mounting <CameraView>, and it is recommended to display a loading screen until then.
import { usePetnowCamera, CameraView } from '@petify/react-native-camera-ui';
function Scan({ apiKey }) {
const camera = usePetnowCamera({ apiKey });
// Mount CameraView only when ready.
if (camera.state.status !== 'ready') {
return <Loading state={camera.state} />; // your own loading component
}
// Mount <CameraView> in Step 2.
return null;
}camera.state
The SDK readiness state. Mount <CameraView> only when status === 'ready'.
| status | Meaning |
|---|---|
idle | apiKey is not set yet |
initializing | Storing the license in the native SDK |
ready | Storage complete — the camera can be mounted |
error | The native initialize call failed (message in state.error) |
state tracks license storage, not validation. License validation is performed at view mount time (initializeCamera) on iOS, and a validation failure surfaces as failed in onDetectionStatus. Android has no license server validation (the key is retained for monitoring purposes). Mounting <CameraView> before ready is not supported.
Step 2: Display the Camera
Once camera.state becomes ready, mount <CameraView>. Mounting initializes the camera, and unmounting cleans it up. Pass the species (species), purpose (purpose), and the server-issued captureSessionId as props.
<CameraView
camera={camera}
species="DOG"
purpose="PET_PROFILE_REGISTRATION"
captureSessionId={captureSessionId}
style={{ flex: 1 }}
onDetectionStatus={setStatus}
onDetectionProgress={setProgress}
onDetectionFinished={onFinished}
/>For how to layer UI such as guide text and buttons on top of <CameraView>, see Customization.
Props
| Prop | Type | Description |
|---|---|---|
camera | PetnowCamera | The controller returned by usePetnowCamera |
species | 'DOG' | 'CAT' | Pet species (determines the detection pipeline) |
purpose | 'PET_PROFILE_REGISTRATION' | 'PET_IDENTIFICATION' | 'PET_VERIFICATION' | Capture purpose (determines the number of required images) |
captureSessionId | string | Server-issued UUID (Server API) |
difficultyMode? | 'EASY' | 'NORMAL' | 'HARD' | If unspecified, the server selects it |
enableFakeDetection? | boolean | Fake-image detection (default false) |
bracketingMode? | boolean | Capture bracketing — briefly burst-shoots around the adopted frame to collect a better fingerprint image (default false). Can be toggled at runtime (no remount needed) |
style | ViewStyle | Standard RN view style |
Changing the session settings (species/purpose/difficultyMode/enableFakeDetection) or captureSessionId automatically triggers a re-initialization flow in the native side on the same view — changing species/purpose/difficultyMode/enableFakeDetection recreates the controller, and changing captureSessionId reconfigures the capture runtime and detector. If you want a clearer transition in terms of UX, you can also re-mount the view with a React key.
The server capture session's petId requirement varies depending on the capture purpose (purpose) — registration and verification require a petId, while identification does not. For details, see Server API – Biometric Data.
Step 3: Receive Events
Detection status, progress, and the finished result are delivered through <CameraView>'s event callbacks.
onDetectionStatus={(s: DetectionStatus) => { /* {type} or {type:'failed', reason} */ }}
onDetectionProgress={(p: number) => { /* 0 ~ 100 */ }}
onDetectionFinished={(r: CameraResult) => { /* {success, fingerprintImages, appearanceImages} */ }}
onDetectionResult={(r: DetectionResult) => { /* per-frame nose/face boxes — for custom markers */ }}onDetectionStatus: the current detection status (DetectionStatus).typeisnoObject|processing|detected|finished|failed(withreasonin this case).onDetectionProgress: progress as an integer from0to100.onDetectionFinished: the final result (CameraResult) —fingerprintImages/appearanceImagesare arrays of localfile://URIs.onDetectionResult: per-frame nose/face detection boxes (DetectionResult). Use it to draw markers yourself; for an example, see Customization.
For the definition of each type, see Types below.
onDetectionProgress and onDetectionResult are called on every detection update (close to frame rate) on the main thread. Keep these handlers lightweight and avoid heavy work or excessive setState.
Step 4: Handle the Result / Upload
The fingerprintImages / appearanceImages from onDetectionFinished are arrays of local file:// URIs. Upload them from JS to your app server, then the server performs registration, verification, and identification via the Server API. The client only handles capture and upload.
Even when capture fails, onDetectionFinished is still called with success: false and empty image arrays (the failure reason is delivered separately via onDetectionStatus failed). Check r.success before using the result.
Keep x-petnow-api-key on your app server only. Do not call the Petnow API directly from the RN client — send the file:// URIs to your app server as shown below, and have the server proxy them to /v2/fingerprints:upload / /v2/appearances:upload.
In React Native, upload a file:// URI via FormData:
async function upload(r: CameraResult) {
if (!r.success) return;
const form = new FormData();
// RN's FormData accepts files as { uri, name, type }.
r.fingerprintImages.forEach((uri, i) =>
form.append('fingerprints', { uri, name: `nose_${i}.jpg`, type: 'image/jpeg' } as any),
);
r.appearanceImages.forEach((uri, i) =>
form.append('appearances', { uri, name: `face_${i}.jpg`, type: 'image/jpeg' } as any),
);
form.append('captureSessionId', captureSessionId);
// Send to your own app-server endpoint → the server uploads to Petnow with x-petnow-api-key
await fetch('https://your-app-server.example.com/petnow/upload', { method: 'POST', body: form });
}Step 5: Additional Features
These are the commands for controlling the camera during and after capture. Commands work after the bound <CameraView> has mounted.
| Method | Description |
|---|---|
camera.startDetection() | Start a detection session (or restart from the beginning — retake) |
camera.pauseDetection() | Pause detection (keeps the camera, preserves progress) |
camera.resumeDetection() | Resume paused detection |
camera.switchCamera() | Switch between front and rear cameras |
camera.retry() | Retry native initialization with the same apiKey (for recovering from a transient error) |
Retake / Continuous Capture
After onDetectionFinished, calling camera.startDetection() on the same view detects again from the beginning (retake). For a continuous flow, simply restart in the completion callback.
const onFinished = useCallback((r: CameraResult) => {
upload(r);
if (continuousMode) {
setTimeout(() => camera.startDetection(), 1200); // restart after a short pause
}
}, [camera, continuousMode]);Types
These are the types used by usePetnowCamera and <CameraView>.
// usePetnowCamera options
type LicenseInfo = {
apiKey: string;
};
// The controller handle returned by usePetnowCamera
type PetnowCamera = {
state: PetnowCameraState;
startDetection(): void; // Start detection / retake
pauseDetection(): void; // Pause (preserves progress)
resumeDetection(): void; // Resume
switchCamera(): void; // Switch front/rear
retry(): void; // Retry native initialization
};
type PetnowCameraState =
| { status: 'idle' }
| { status: 'initializing' }
| { status: 'ready' }
| { status: 'error'; error: string }; // error message only on the 'error' state
// onDetectionStatus callback argument
type DetectionStatus =
| { type: 'noObject' } // No target detected
| { type: 'processing' } // Detection in progress
| { type: 'detected' } // Detection successful
| { type: 'finished' } // Capture complete
| { type: 'failed'; reason: DetectionFailureReason }; // Failed (includes reason)
// onDetectionFinished callback argument
type CameraResult = {
success: boolean;
fingerprintImages: string[]; // local file:// URIs
appearanceImages: string[]; // local file:// URIs
};
// onDetectionResult callback argument (per-frame detection boxes)
type BoundingBox = { x: number; y: number; width: number; height: number }; // normalized 0–1
type DetectionResult = {
nose: BoundingBox | null;
face: BoundingBox | null;
};Error / Permission Handling
There is no separate error channel. Failures surface in two places.
- Initialization (license storage) failure →
camera.statebecomeserror. Retry withcamera.retry(). - Camera/detection stage failure (invalid license (iOS), permission denied, camera open failure, capture failure) →
{ type: 'failed', reason }inonDetectionStatus.
function guideMessage(status: DetectionStatus | null): string {
if (!status) return 'Initializing camera...';
switch (status.type) {
case 'failed':
return `Recognition failed: ${status.reason}`;
case 'noObject':
return 'Center your pet on the screen';
case 'finished':
return 'Capture complete!';
default:
return '';
}
}When permission is denied, failed is delivered. Guide the user to grant the permission in Settings, then re-mount <CameraView>.
Two-Tier Lifecycle
The camera (view lifetime) is the outer layer, and detection is a sub-lifecycle that runs within it. For the diagram and a detailed explanation, see Introduction.
Automatic Session Lifetime
The camera is a single hardware resource, but RN views are frequently destroyed and recreated, so the session is managed by the package as a singleton rather than by the view. With a 1.5-second grace period after the last detach, the session is reused across transitions and remounts, and when captureSessionId changes, the capture runtime and detector are reconfigured with the new session ID while keeping the controller/session ownership structure intact. For details, see Introduction.
Attention Sounds
You can play attention sounds (PetnowSound) to draw a pet's attention independently of the camera. For the full sound list, previews, and API, see the Sound Guide.
Complete Code
This example combines the hook, the ready gate, <CameraView>, events, and upload into a single component. The captureSessionId is received as a prop, issued by your app server via the Server API.
import { useCallback, useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import {
usePetnowCamera,
CameraView,
type DetectionStatus,
type CameraResult,
} from '@petify/react-native-camera-ui';
export function Scan({ apiKey, captureSessionId }: { apiKey: string; captureSessionId: string }) {
const camera = usePetnowCamera({ apiKey });
const [, setStatus] = useState<DetectionStatus | null>(null);
// Upload file:// URIs to your app server (which proxies to Petnow with x-petnow-api-key)
const upload = useCallback(
async (r: CameraResult) => {
if (!r.success) return;
const form = new FormData();
r.fingerprintImages.forEach((uri, i) =>
form.append('fingerprints', { uri, name: `nose_${i}.jpg`, type: 'image/jpeg' } as any),
);
r.appearanceImages.forEach((uri, i) =>
form.append('appearances', { uri, name: `face_${i}.jpg`, type: 'image/jpeg' } as any),
);
form.append('captureSessionId', captureSessionId);
await fetch('https://your-app-server.example.com/petnow/upload', { method: 'POST', body: form });
},
[captureSessionId],
);
// Do not mount CameraView before ready.
if (camera.state.status !== 'ready') {
return (
<View style={styles.center}>
<Text>Preparing camera… ({camera.state.status})</Text>
</View>
);
}
return (
<CameraView
camera={camera}
species="DOG"
purpose="PET_PROFILE_REGISTRATION"
captureSessionId={captureSessionId}
style={styles.fill}
onDetectionStatus={setStatus}
onDetectionFinished={upload}
/>
);
}
const styles = StyleSheet.create({
fill: { flex: 1 },
center: { flex: 1, alignItems: 'center', justifyContent: 'center' },
});Next Steps
- Customization — Layering guide, buttons, and result UI on top of
<CameraView> - Sound Guide — Playing attention sounds