Basic Usage
How to integrate the camera UI with the usePetnowCamera hook and CameraView.
Full Example
import { useCallback, useState } from 'react';
import {
usePetnowCamera,
CameraView,
type DetectionStatus,
type CameraResult,
} from '@petnow/react-native-camera-ui';
function Scan({ apiKey, captureSessionId }) {
const camera = usePetnowCamera({ apiKey });
const [status, setStatus] = useState<DetectionStatus | null>(null);
const [progress, setProgress] = useState(0);
const onFinished = useCallback((r: CameraResult) => {
if (r.success) upload(r.fingerprintImages, r.appearanceImages);
}, []);
// Mount CameraView only when ready.
if (camera.state.status !== 'ready') {
return <Loading state={camera.state} />;
}
return (
<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.
usePetnowCamera
const camera = usePetnowCamera({ apiKey, isDebugMode });Stores apiKey in the native SDK and returns a controller handle with detection commands. When apiKey changes, it is stored again.
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.
Commands
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) |
CameraView
Binds to the controller and renders the native preview + detection overlay. Mounting initializes the camera, and unmounting cleans it up.
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.
Events
onDetectionStatus={(s: DetectionStatus) => { /* {type} or {type:'failed', reason} */ }}
onDetectionProgress={(p: number) => { /* 0 ~ 100 */ }}
onDetectionFinished={(r: CameraResult) => { /* {success, fingerprintImages, appearanceImages} */ }}DetectionStatus.type:noObject|processing|detected|finished|failed(withreasonin this case).CameraResult:fingerprintImages/appearanceImagesare arrays of localfile://URIs.
Two-Layer Lifecycle
The camera (view lifetime) is the outer layer, and detection is a sub-lifecycle that runs within it.
Camera: mount ────────────────────────────── unmount
(initializeCamera) (finalize)
Detection: └ auto start → pause ⇄ resume → finished → retake(startDetection) ┘- The camera follows the view lifetime (mount =
initializeCamera, unmount =finalize). - Detection starts automatically on mount; pause, resume, and retake (
startDetection) afterward are controlled by hook commands (pauseDetectionpreserves progress).
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>.
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]);Result Handling / 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 (the flow is identical to the native test app).
Session Lifetime Automation
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 (PetnowSound)
You can play attention sounds to draw a pet's attention independently of the camera (corresponding to the native SoundPlayer). It works even without a camera view.
import { PetnowSound } from '@petnow/react-native-camera-ui';
await PetnowSound.prepare(); // Load samples (first time only)
const handle = PetnowSound.play('random', 'DOG'); // Play → returns a stop handle
// ...
PetnowSound.stop(handle); // Stop with the handle
PetnowSound.release(); // Release native audio resources| Method | Description |
|---|---|
prepare() | Load samples (Promise). Once before play. iOS resolves immediately (on-demand load) |
play(sound, species) | Returns a stop handle (number, ≥0) after playing. -1 if not prepared/not mapped |
stop(handle) | Stop with the handle |
release() | Release native audio resources |
sound:'random'(selects a candidate suitable for the species) |'pouringFood'|'bagOfChips'|'openingCan'|'squeakyToy'|'pspsps'|'shakers'species:'DOG'|'CAT'— used for'random'candidate selection (ignored for other sounds)
Next Steps
- Customization — Layering guide, buttons, and result UI on top of
<CameraView>