@smileid/usesmileid
v12.0.0
Published
Official Smile ID React Native (Expo) SDK for identity verification: selfie capture, liveness checks, and document verification powered by on-device ML.
Downloads
271
Readme
Smile ID React Native (Expo) SDK
The UseSmileID React Native SDK lets you embed identity verification flows into your Expo or bare React Native app using a type-safe DSL builder. Compose screens like LEGO bricks — no predefined product flows, no inheritance.
SDK Size
Sizes are measured on every push to main and updated automatically by CI.
| Package | Download Size | Install Size |
| ---------------------------------------- | ------------- | ------------ |
| @smileid/usesmileid | — | — |
| @smileid/usesmileid_bridge | — | — |
| @smileid/usesmileid_platform_interface | — | — |
| @smileid/usesmileid_mlkit_face | — | — |
| @smileid/usesmileid_mlkit_document | — | — |
| @smileid/usesmileid_huawei_face | — | — |
| @smileid/usesmileid_huawei_document | — | — |
| @smileid/usesmileid_vision_face | — | — |
| @smileid/usesmileid_vision_document | — | — |
| Sample app (iOS) | — | — |
| Sample app (Android) | — | — |
Requirements
- React Native 0.83+ or Expo SDK 55+
- Node 20.18+
- React 19.2+
- Xcode (latest stable) — iOS 15.0+ deployment target
- Android Gradle Plugin 8.x —
compileSdk37,minSdk26 (thecom.usesmileid:*12.0.0 AARs are built with compileSdk 37 and enforce it as the consumer minimum — setandroid.compileSdkVersion=37ingradle.propertiesif your project pins a lower level.minSdk26 is required because VisionCamera'sFrame.toArrayBuffer(), which the SDK's frame streaming depends on, is compiled out below it — setandroid.minSdkVersion=26)
Installation
Add the SDK, its native peer dependencies, and the analyzer providers that match your target platforms:
pnpm add @smileid/usesmileid \
react-native-quick-crypto react-native-quick-base64 react-native-nitro-modules \
@smileid/usesmileid_mlkit_face @smileid/usesmileid_mlkit_document \
@smileid/usesmileid_vision_face @smileid/usesmileid_vision_document(Use @smileid/usesmileid_huawei_{face,document} instead of mlkit on no-GMS Android targets.)
react-native-quick-crypto, react-native-quick-base64, and react-native-nitro-modules are required peer dependencies — the SDK uses them for native request signing (PBKDF2/HMAC). They must be installed in your app directly: React Native autolinking only links native modules declared in the app's own package.json, not a library's transitive ones, so the SDK cannot pull them in for you.
The provider packages each have native code (Android AAR / iOS SPM). Expo prebuild picks them up automatically via expo-module.config.json. For bare React Native, run npx pod-install after adding the packages and let Gradle resolve the AARs on the next Android build.
Quick Start
Mount <UseSmileIDBuilder> anywhere in your component tree. It is a regular React component — use it like any other.
import { CaptureType, JobType, UseSmileIDBuilder } from "@smileid/usesmileid";
import { faceAnalyzer } from "./src/providers";
export default function VerificationScreen() {
return (
<UseSmileIDBuilder
builder={(b) => {
b.network((n) => {
n.config((c) => {
c.jobType = JobType.smartSelfieEnrollment;
c.token = "your-v3-token";
c.partnerConfig((p) => {
p.partnerId = "your-partner-id";
});
});
});
b.ml((m) => {
m.analyzers((a) => {
a.forCaptureType(CaptureType.selfie, (face) =>
face.add(faceAnalyzer),
);
});
});
b.screens((screens) => {
screens.instructions();
screens.capture((c) => {
c.captureType = CaptureType.selfie;
});
screens.preview();
screens.processing();
});
b.onResult = (result) => {
switch (result.status) {
case "success":
console.log("Captured:", result.value);
break;
case "failure":
console.error("Error:", result.error);
break;
}
};
}}
/>
);
}./src/providers is a pair of platform-suffix files (providers.ios.ts and providers.android.ts) that select the right native analyzer per platform. See Federated providers below.
Full Builder Reference
useSmileIDBuilder takes a callback that receives a mutable builder. Top-level mutable properties (consentInformation, enhancedKYCParams, biometricKYCParams, documentVerificationParams, enhancedDocumentVerificationParams, userDetails, localizations, onResult, onAnalyticsEvent) are property-assigned. Nested blocks (config, theme, network, ml, screens) take closures.
The server issues userId and jobId and returns them in the submission response — partners receive both via onResult. Don't construct them client-side.
config — Global settings
b.config((c) => {
c.enableDebugMode = false;
c.allowOfflineMode = false;
c.enableCrashReporting = true;
});| Property | Type | Default | Description |
| ---------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ |
| enableDebugMode | boolean | false | Shows a validation error screen on build failure |
| allowOfflineMode | boolean | false | Allows the flow to run without a network connection |
| enableCrashReporting | boolean | true | Enables Sentry crash reporting for the SDK. Set to false if your app runs its own Sentry |
theme — UI customisation
Each color slot accepts an AdaptiveColor {light, dark} pair via the color({...}) helper. The SDK resolves the active variant at mount time using the device's color scheme — partners supply both values up front.
b.theme((t) => {
t.primaryColor = t.color("#1A73E8", "#4DA3FF");
t.primaryForeground = t.color("#FFFFFF", "#000000");
t.secondaryColor = t.color("#5F6368", "#9AA0A6");
t.accentColor = t.color("#34A853", "#81C995");
t.fontFamily = "Inter";
t.buttonShape = 12;
t.cardShape = 16;
});| Property | Type | Default | Description |
| ------------------- | --------------- | ----------- | ------------------------------------------------------ |
| primaryColor | AdaptiveColor | SDK default | Main action colour (buttons, highlights) |
| primaryForeground | AdaptiveColor | SDK default | Text/icons on primary colour |
| secondaryColor | AdaptiveColor | SDK default | Secondary surface colour |
| accentColor | AdaptiveColor | SDK default | Accent highlights |
| fontFamily | string | SDK default | Font family loaded via expo-font or RN's font system |
| buttonShape | number (dp) | SDK default | Corner radius for buttons |
| cardShape | number (dp) | SDK default | Corner radius for cards |
Both defaultLightTheme and defaultDarkTheme are exported. They share typography / spacing / shapes and differ only in colors — partners can mix-and-match.
Localization
Every string rendered by the SDK can be overridden at the app level. Drop one translation JSON per locale, register once at app startup, and the SDK auto-resolves the device locale at flow mount.
import { UseSmileIDLocalizations } from "@smileid/usesmileid";
import frOverrides from "./l10n/fr.json";
import arOverrides from "./l10n/ar.json";
UseSmileIDLocalizations.register("fr", frOverrides);
UseSmileIDLocalizations.register("ar", arOverrides);// l10n/fr.json
{
"si_consent_allow": "Accepter",
"si_consent_consent": "Je teste juste des trucs au hasard",
}Match strategy: exact match ('pt-BR') → language-only fallback ('pt') → English defaults.
Override the device-locale resolver to plug in your own resolution (e.g. expo-localization):
import { getLocales } from "expo-localization";
UseSmileIDLocalizations.localeResolver = () =>
getLocales()[0]?.languageCode ?? "en";Per-flow override (wins over the registry) is still available via b.localizations = UseSmileIDLocalizations.fromOverrides(map).
network — API configuration
All fields are optional. Omit the network block entirely to use SDK defaults.
import { JobType, LogLevel } from "@smileid/usesmileid";
b.network((n) => {
n.config((c) => {
c.jobType = JobType.documentVerification;
c.token = "your-v3-token";
// Optional: refresh on 401 mid-flow. Invoked when the server returns
// 401; return a fresh token and the SDK retries once.
c.onTokenExpired = async (previous) => fetchFreshToken();
c.partnerConfig((p) => {
p.partnerId = "your-partner-id";
p.callbackUrl = "https://example.com/callback";
p.useSandbox = false;
});
c.logging((l) => {
l.enabled = true;
l.level = LogLevel.basic;
});
});
n.timeouts((t) => {
t.connect = 30;
t.read = 60;
t.write = 60;
t.call = 120;
});
n.cache((c) => {
c.enabled = true;
c.maxSize = 100 * 1024 * 1024; // 100 MB
});
n.interceptors((i) => {
i.add(i.gzip());
i.add(
i.logging((l) => {
l.level = LogLevel.body;
}),
);
i.add(myCustomInterceptor); // partner-supplied
});
});config block
| Property | Type | Default | Description |
| ---------------- | ----------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| jobType | JobType | JobType.unknown | Job type sent with every request |
| token | string | "" | Short-lived v3 auth token. Exchange your long-lived API key for one from your own backend (POST /v3/token) and supply it here. The SDK stamps it on every authed request. |
| onTokenExpired | ((previousToken: string) => Promise<string>) \| undefined | undefined | Optional refresh callback. Invoked on a 401 Unauthorized response on an authed request. The SDK calls it with the token it was using, expects a fresh token back, and retries the failed request once. Concurrent 401s collapse to a single callback invocation. If the callback throws or the retry also returns 401, the original 401 surfaces unchanged. |
partnerConfig block
| Property | Type | Default | Description |
| --------------- | ------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| partnerId | string | "" | Your Smile ID partner ID |
| callbackUrl | string | "" | Webhook URL for job results |
| useSandbox | boolean | false | Route requests to the sandbox environment |
| partnerParams | Record<string, string>? | undefined | Optional partner-defined key/value pairs forwarded as the partner_params JSON multipart part on Enhanced/Biometric KYC submissions |
logging block
| Property | Type | Default | Description |
| --------- | ---------- | ---------------- | ------------------------------------- |
| enabled | boolean | true | Enable network logging |
| level | LogLevel | LogLevel.basic | none / basic / headers / body |
timeouts block (seconds)
| Property | Default |
| --------- | ------- |
| connect | 60 |
| read | 60 |
| write | 60 |
| call | 120 |
cache block
| Property | Type | Default |
| --------- | ---------------- | -------------------- |
| enabled | boolean | true |
| maxSize | number (bytes) | 52_428_800 (50 MB) |
ml — Federated machine-learning analyzers
Pick the analyzer that matches your platform / device fleet:
| Platform | Face | Document | Native dep |
| ---------------- | --------------------------------- | ------------------------------------- | --------------------- |
| iOS | @smileid/usesmileid_vision_face | @smileid/usesmileid_vision_document | Apple Vision (SPM) |
| Android (GMS) | @smileid/usesmileid_mlkit_face | @smileid/usesmileid_mlkit_document | Google ML Kit (Maven) |
| Android (no GMS) | @smileid/usesmileid_huawei_face | @smileid/usesmileid_huawei_document | Huawei HMS (Maven) |
import { CaptureType } from "@smileid/usesmileid";
import { faceAnalyzer, documentAnalyzer } from "./src/providers";
b.ml((m) => {
m.analyzers((a) => {
a.forCaptureType(CaptureType.selfie, (face) => face.add(faceAnalyzer));
a.forCaptureType(CaptureType.document, (doc) => doc.add(documentAnalyzer));
});
});Important: each provider package's TS index calls requireNativeModule at module-load. Importing @smileid/usesmileid_mlkit_face on iOS (or @smileid/usesmileid_vision_face on Android) crashes the bundle at evaluation time. Use platform-suffix files so Metro picks the right one and the wrong-platform package never loads:
// providers.ios.ts
import { useSmileIDVisionFace } from "@smileid/usesmileid_vision_face";
import { useSmileIDVisionDocument } from "@smileid/usesmileid_vision_document";
export const faceAnalyzer = useSmileIDVisionFace;
export const documentAnalyzer = useSmileIDVisionDocument;
// providers.android.ts
import { useSmileIDMlkitFace } from "@smileid/usesmileid_mlkit_face";
import { useSmileIDMlkitDocument } from "@smileid/usesmileid_mlkit_document";
export const faceAnalyzer = useSmileIDMlkitFace;
export const documentAnalyzer = useSmileIDMlkitDocument;Add "moduleSuffixes": [".ios", ".android", ""] to your tsconfig.json so tsc resolves ./src/providers correctly. See sample-expo/ for a complete reference.
screens — Flow composition
Call screen functions directly inside the screens closure. The flow navigates through them in the order they are declared.
import { CaptureType } from "@smileid/usesmileid";
b.screens((screens) => {
screens.consent((c) => {
c.partnerName = "Acme Corp";
c.partnerIcon = <Image source={require("./assets/logo.png")} style={{ width: 36, height: 36 }} />;
c.partnerPrivacyPolicyUrl = "https://acme.com/privacy";
c.showAttribution = true;
c.onConsentGranted = (info) => console.log("Consent:", info);
});
screens.instructions((i) => {
i.title = "Verify your identity";
i.description = "You will need a selfie and a document.";
i.showAttribution = true;
});
screens.permission((p) => {
p.cameraRationale = "Camera access required.";
});
screens.capture((cap) => {
cap.captureType = CaptureType.selfie;
cap.selfie((sel) => {
sel.allowAgentMode = false;
sel.enableEnhancedLiveness = true;
});
});
screens.capture((cap) => {
cap.captureType = CaptureType.document;
cap.document((doc) => {
doc.autoCapture = true;
doc.autoCaptureTimeout = 10;
doc.allowGalleryUpload = false;
doc.captureBothSides = true;
doc.allowSkipBack = false;
doc.knownIdAspectRatio = 1.586; // CR-80 card ratio (optional)
});
});
screens.preview((p) => {
p.allowRetake = true;
});
screens.processing((p) => {
p.showProgressPercentage = true;
});
});Screen types
| Screen | How to add | Key properties |
| ---------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Consent | screens.consent((c) => ...) | partnerName, partnerIcon, partnerPrivacyPolicyUrl, showAttribution, onConsentGranted, allowButton, denyButton (partnerIcon and partnerPrivacyPolicyUrl are required) |
| Instructions | screens.instructions((i) => ...) | title, description, showAttribution, continueButton |
| Permission | screens.permission((p) => ...) | cameraRationale, continueButton |
| Selfie capture | screens.capture((cap) => { cap.captureType = CaptureType.selfie; cap.selfie(...); }) | allowAgentMode, enableEnhancedLiveness |
| Document capture | screens.capture((cap) => { cap.captureType = CaptureType.document; cap.document(...); }) | autoCapture, allowGalleryUpload, captureBothSides, allowSkipBack, knownIdAspectRatio |
| Preview | screens.preview((p) => ...) | allowRetake, continueButton, cancelButton |
| Processing | screens.processing((p) => ...) | showProgressPercentage, continueButton, retryButton, exitButton |
Button slots receive { onPress: () => void; isEnabled: boolean } and return any React node, so partners can ship fully custom buttons without losing the orchestrator's wiring.
Custom screens & button slots — useFlowNavigation()
Inside any partner component rendered under <UseSmileIDBuilder> (custom button slot, fully replaced screen, partner widget), call useFlowNavigation() to drive the flow without prop-drilling.
import { useFlowNavigation } from "@smileid/usesmileid";
import { Pressable, Text } from "react-native";
function ExitButton() {
const nav = useFlowNavigation();
return (
<Pressable onPress={() => nav.exit()}>
<Text>Cancel</Text>
</Pressable>
);
}The hook returns the active FlowNavigationManager:
| Method | Behaviour |
| --------------- | --------------------------------------------------------------------------------------------------------------- |
| next() | Advance to the next screen. From the last step, calls onResult({ status: 'success', ... }). |
| back() | Decrement. From the first step, calls onResult({ status: 'failure', error: ... }) with cancelled semantics. |
| exit(reason?) | Exit immediately — useful for "abort" buttons. Defaults to cancelled. |
| captured | Snapshot of artifacts collected so far (selfiePath, livenessPaths, frontDocumentPath, etc). |
Calling useFlowNavigation() outside <UseSmileIDBuilder> throws — the misuse is loud at first render rather than silently no-op-ing.
Top-level builder callbacks
| Property | Type | Description |
| ------------------ | ----------------------------------------------------------- | --------------------------------------------------------- |
| onResult | (result: UseSmileIDResult<JobSubmissionResponse>) => void | Called once when the flow finishes (success or failure) |
| onAnalyticsEvent | ((event: UseSmileIDAnalyticsEvent) => void) \| undefined | Optional. Called for each analytics event during the flow |
Job-specific parameters
Each job type that needs identity fields reads them from a top-level builder property. The shared user_details part (b.userDetails) is required by every KYC and document job; country is the ISO 3166-1 alpha-2 code.
| Job type | Builder property | Required fields | Notes |
| -------------------------------------- | -------------------------------------- | ------------------------------- | ------------------------------------------------------------------------ |
| JobType.enhancedKyc | b.enhancedKYCParams | idType, idNumber, country | Optional bankCode, mobileOperator |
| JobType.biometricKyc | b.biometricKYCParams | idType, idNumber, country | useEnrolledImage toggles the enrolled-image path |
| JobType.documentVerification | b.documentVerificationParams | country | idType optional — omit it to let the backend classify the document |
| JobType.enhancedDocumentVerification | b.enhancedDocumentVerificationParams | country, idType | idType required — drives the ID-authority cross-check |
// Document Verification — idType optional
b.documentVerificationParams = { country: "KE" };
b.userDetails = {
givenNames: "Jane",
lastName: "Doe",
email: "[email protected]",
};
// Enhanced Document Verification — idType required
b.enhancedDocumentVerificationParams = { country: "KE", idType: "NATIONAL_ID" };
b.userDetails = {
givenNames: "Jane",
lastName: "Doe",
phoneNumber: "+254700000000",
};user_details requires non-blank givenNames and lastName plus at least one of email / phoneNumber. Pre-flight any payload with b.validateDocumentVerificationParams(...), b.validateEnhancedDocumentVerificationParams(...), or b.validateUserDetails(...) before assigning.
Handling results
Set b.onResult before (or during) the builder block. It is called once when the flow finishes.
b.onResult = (result) => {
switch (result.status) {
case "success":
console.log(`Job ${result.value.jobId} status=${result.value.status}`);
break;
case "failure":
console.error("flow failed:", result.error);
break;
}
};The success branch carries a JobSubmissionResponse with jobId, userId, status, message, and an optional createdAt. Partner-supplied inputs (captured frames, identity fields, consent) are not echoed back — keep references at the call site if you need them after the flow.
UseSmileIDResult<T> is a discriminated union — result.status narrows it, no try / catch needed.
Forwarding failures to a crash reporter
Every exception the SDK surfaces extends UseSmileIDException, which carries:
errorCode— a stable UPPER_SNAKE_CASE string (e.g."NETWORK_TIMEOUT","BUILDER_VALIDATION_ERROR") suitable for grouping. Stable across SDK releases, so Sentry pivots and analytics dashboards keyed on it keep working after upgrades.message— partner-safe message; never contains PII.suggestedFix— concrete remediation hint for triage.cause— the wrapped underlying error (AxiosError/Error) for stack trace forwarding.
Forward failures to Sentry, Bugsnag, or Crashlytics from inside the failure branch of onResult:
import * as Sentry from "@sentry/react-native";
import { UseSmileIDException } from "@smileid/usesmileid";
b.onResult = (result) => {
if (result.status === "success") {
myAnalytics.recordSuccess(result.value.jobId);
return;
}
const error = result.error;
if (error instanceof UseSmileIDException) {
Sentry.captureException(error.cause ?? error, {
tags: { "smileid.error_code": error.errorCode ?? "unknown" },
extra: { "smileid.suggested_fix": error.suggestedFix },
});
}
};The wrapped cause carries the original stack trace so crash-reporter grouping keys off the underlying transport / IO failure, not the SDK's wrapper class. errorCode rides as a tag so you can filter the crash board by failure category.
Analytics
Set b.onAnalyticsEvent to receive a stream of events fired at key moments in the flow. Each event carries a flat Record<string, string> that you can forward directly to any analytics backend.
import * as Analytics from "expo-firebase-analytics";
b.onAnalyticsEvent = (event) => {
// Forward to Firebase, Mixpanel, Segment, or your own backend
Analytics.logEvent(event.type, event.extras);
};Every event automatically includes session_id and timestamp (epoch milliseconds) so you can correlate events across a single flow run without any extra bookkeeping.
Event reference
| Event | When fired | Key extras |
| ------------------- | ---------------------------------- | --------------------------------------------------------------------------------- |
| flow_started | Flow initialises | job_type |
| screen_viewed | Each screen becomes active | screen_name |
| consent_captured | User grants consent | decision: "granted" |
| selfie_captured | Selfie + liveness captured | liveness_image_count |
| document_captured | Document image captured | document_side: "front" or "both" |
| retake_requested | User navigates back to redo a step | — |
| job_submitted | API submission begins | job_type, attempt |
| flow_completed | Flow finishes | result: "success" or "failure", job_id (success), error_message (failure) |
onAnalyticsEvent is undefined by default — omit it and no events are delivered.
Pre-supplied consent
If the user already consented in a previous session, skip the consent screen by supplying consentInformation before building:
import {
UseSmileIDBuilder,
type ConsentInformation,
} from "@smileid/usesmileid";
<UseSmileIDBuilder
builder={(b) => {
const consentInfo: ConsentInformation = {
/* … */
};
b.consentInformation = consentInfo;
b.screens((screens) => {
// do not add a consent screen when consentInformation is set —
// the builder will reject it
screens.instructions();
screens.capture((c) => {
c.captureType = CaptureType.selfie;
});
});
}}
/>;If consentInformation is malformed (missing required fields, invalid timestamps), <UseSmileIDBuilder> routes the failure through onResult.failure carrying a typed BuilderValidationException — see Validation below.
Validation
<UseSmileIDBuilder> validates eagerly: it surfaces every missing or invalid field the moment the closure returns. Partners receive a typed BuilderValidationException via onResult.failure — error.validationState.issues carries every rule that failed, not just the first.
<UseSmileIDBuilder
builder={(b) => {
// …
b.onResult = (result) => {
if (
result.status === "failure" &&
result.error instanceof BuilderValidationException
) {
for (const issue of result.error.validationState.issues) {
console.error("[SmileID]", issue.errorCode, issue.message);
}
}
};
}}
/>Triggers cover the validation rules — empty screens, ordering invariants, duplicate non-capture screens, capture without ML config, processing without network config, unknown job type at processing time, double-source consent, malformed consentInformation, blank KYC params. Set b.config((c) => { c.enableDebugMode = true; }) to additionally render ValidationErrorScreen on-device with every failed rule during development. In production the screen does not mount — partners only see the failure via onResult.failure.
Full example — Document verification
import { Alert, StyleSheet } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import {
CaptureType,
JobType,
LogLevel,
UseSmileIDBuilder,
} from "@smileid/usesmileid";
import { faceAnalyzer, documentAnalyzer } from "./src/providers";
export function DocumentVerificationScreen({
onComplete,
}: {
onComplete: () => void;
}) {
return (
<SafeAreaView style={styles.container} edges={["top", "bottom"]}>
<UseSmileIDBuilder
builder={(b) => {
// Document Verification requires its params + userDetails.
b.documentVerificationParams = {
country: "KE",
idType: "NATIONAL_ID",
};
b.userDetails = {
givenNames: "Jane",
lastName: "Doe",
email: "[email protected]",
};
b.config((c) => {
c.enableDebugMode = false;
c.enableCrashReporting = true;
});
b.theme((t) => {
t.primaryColor = t.color("#1A73E8", "#4DA3FF");
t.buttonShape = 12;
});
b.network((n) => {
n.config((c) => {
c.jobType = JobType.documentVerification;
c.token = "your-v3-token";
c.partnerConfig((p) => {
p.partnerId = "your-partner-id";
p.callbackUrl = "https://example.com/callback";
p.useSandbox = true;
});
c.logging((l) => {
l.enabled = true;
l.level = LogLevel.basic;
});
});
n.timeouts((t) => {
t.connect = 30;
t.call = 120;
});
});
b.ml((m) => {
m.analyzers((a) => {
a.forCaptureType(CaptureType.selfie, (face) =>
face.add(faceAnalyzer),
);
a.forCaptureType(CaptureType.document, (doc) =>
doc.add(documentAnalyzer),
);
});
});
b.screens((screens) => {
screens.consent((c) => {
c.partnerName = "Acme Corp";
c.partnerPrivacyPolicyUrl = "https://acme.com/privacy";
});
screens.instructions((i) => {
i.title = "Verify your identity";
i.description = "You will need a selfie and a document.";
});
screens.capture((cap) => {
cap.captureType = CaptureType.document;
cap.document((doc) => {
doc.autoCapture = true;
doc.captureBothSides = true;
doc.allowGalleryUpload = false;
});
});
screens.preview((p) => {
p.allowRetake = true;
});
screens.processing();
});
b.onResult = (result) => {
switch (result.status) {
case "success":
Alert.alert("Done", JSON.stringify(result.value));
break;
case "failure":
Alert.alert("Failed", String(result.error));
break;
}
onComplete();
};
b.onAnalyticsEvent = (event) => {
console.log(
`[SmileID] ${event.type} — session: ${event.extras["session_id"]}`,
event.extras,
);
};
}}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#FFFFFF" },
});