Petnow LogoPetnow

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'.

statusMeaning
idleapiKey is not set yet
initializingStoring the license in the native SDK
readyStorage complete — the camera can be mounted
errorThe 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

PropTypeDescription
cameraPetnowCameraThe 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)
captureSessionIdstringServer-issued UUID (Server API)
difficultyMode?'EASY' | 'NORMAL' | 'HARD'If unspecified, the server selects it
enableFakeDetection?booleanFake-image detection (default false)
bracketingMode?booleanCapture bracketing — briefly burst-shoots around the adopted frame to collect a better fingerprint image (default false). Can be toggled at runtime (no remount needed)
styleViewStyleStandard 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). type is noObject | processing | detected | finished | failed (with reason in this case).
  • onDetectionProgress: progress as an integer from 0 to 100.
  • onDetectionFinished: the final result (CameraResult) — fingerprintImages / appearanceImages are arrays of local file:// 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.

MethodDescription
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) failurecamera.state becomes error. Retry with camera.retry().
  • Camera/detection stage failure (invalid license (iOS), permission denied, camera open failure, capture failure) → { type: 'failed', reason } in onDetectionStatus.
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

On this page