@maiguard-hq/verify-react-native
v0.1.4
Published
MaiGuard Verify SDK for React Native — guided face capture, provider liveness, and facial verification
Maintainers
Readme
@maiguard-hq/verify-react-native
MaiGuard Verify for React Native provides a branded face-capture experience, continuous face framing and lighting guidance, reference-image-agnostic 1:1 face matching, and an integration boundary for provider-attested liveness.
The integrating business supplies a customer reference, verification purpose, user consent, and trusted portrait. MaiGuard controls the capture experience, verification policy, and final decision.
Choose the correct mode
| Environment | Liveness | Intended use |
| --- | --- | --- |
| Sandbox | Built-in blink and head-turn challenge | UI development and integration testing only |
| Production | Approved provider adapter through providerLivenessHandler | Real identity verification |
The sandbox challenge confirms continuous face tracking and user movement, but it is not provider-attested presentation-attack detection. The SDK rejects the sandbox challenge when a pk_live_* key or environment: 'live' is used.
Production integrations must be provisioned for a MaiGuard-supported liveness provider. The provider adapter performs the official native capture and returns an opaque session ID. Do not build a production decision from the client-side blink and turn signals.
Requirements
| Requirement | Minimum |
| --- | --- |
| React | 18 |
| React Native | 0.73 |
| iOS | 15.5, or the higher minimum required by the host framework |
| Android | API 26 |
| react-native-svg | 15 |
| react-native-vision-camera | 4.5 |
| react-native-vision-camera-face-detector | 1.8 |
| react-native-worklets-core | 1.5 |
- Use a physical device for camera and liveness testing.
- Expo Go is not supported because this package contains native code. Use an Expo development build, EAS build, or bare React Native build.
- The microphone is not required.
- Installing or upgrading the SDK requires a new native build. Reloading Metro is not sufficient.
Before integrating, obtain:
- A MaiGuard public key (
pk_test_*for sandbox orpk_live_*for production). - A stable customer reference from your system.
- A trusted reference portrait or an opaque reference held by your backend.
- Explicit biometric consent from the user.
- For production, the provider adapter and server-side authorization configured during MaiGuard onboarding.
Public keys may be bundled in a mobile application. Never bundle a secret key, provider credential, or privileged backend credential.
Expo installation
Install the SDK and compatible native dependencies:
npm install @maiguard-hq/verify-react-native
npx expo install react-native-svg react-native-vision-camera \
react-native-vision-camera-face-detector react-native-worklets-core \
expo-image-manipulator expo-build-propertiesAdd camera permissions, native platform minimums, and frame-processor support to app.json:
{
"expo": {
"plugins": [
[
"react-native-vision-camera",
{
"cameraPermissionText": "Allow $(PRODUCT_NAME) to use the camera for identity verification.",
"enableMicrophonePermission": false,
"enableFrameProcessors": true
}
],
[
"expo-build-properties",
{
"android": { "minSdkVersion": 26 }
}
]
]
}
}Enable Worklets in babel.config.js:
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['react-native-worklets-core/plugin'],
};
};Generate and run the native project:
npx expo prebuild
npx expo run:ios
# or
npx expo run:androidAfter changing the Babel configuration, restart Metro with npx expo start --clear.
Bare React Native installation
Install the SDK and peer dependencies:
npm install @maiguard-hq/verify-react-native \
react-native-svg \
react-native-vision-camera \
react-native-vision-camera-face-detector \
react-native-worklets-coreThen:
- Add
NSCameraUsageDescriptiontoios/YourApp/Info.plist. - Set the iOS deployment target to 15.5 or later.
- Add
android.permission.CAMERAtoandroid/app/src/main/AndroidManifest.xml. - Set Android
minSdkVersionto 26 or later. - Add
react-native-worklets-core/plugintobabel.config.jsas shown above. - Run
npx pod-installand rebuild both native applications.
No microphone permission or manual React Native module linking is required.
Complete sandbox example
This example uses an already compressed reference portrait and compresses the captured selfie before submission. Keep the component inside a parent with a defined size; a full-screen container should use flex: 1.
import { useMemo } from 'react';
import { Text, View } from 'react-native';
import { manipulateAsync, SaveFormat } from 'expo-image-manipulator';
import {
MaiGuardFaceVerification,
type FaceVerificationResult,
type PhotoEncoder,
} from '@maiguard-hq/verify-react-native';
const publicKey = process.env.EXPO_PUBLIC_MAIGUARD_VERIFY_KEY;
const encodeCapturedPhoto: PhotoEncoder = async (path) => {
const uri = path.startsWith('file://') ? path : `file://${path}`;
const image = await manipulateAsync(
uri,
[{ resize: { width: 720 } }],
{ base64: true, compress: 0.72, format: SaveFormat.JPEG },
);
if (!image.base64) throw new Error('The captured image could not be encoded.');
return `data:image/jpeg;base64,${image.base64}`;
};
type Props = {
customerId: string;
referenceImageDataUrl: string;
consentGranted: boolean;
onClose: () => void;
onVerified: (result: FaceVerificationResult) => void;
};
export function IdentityVerificationScreen({
customerId,
referenceImageDataUrl,
consentGranted,
onClose,
onVerified,
}: Props) {
const request = useMemo(() => ({
customerId,
verificationPurpose: 'onboarding' as const,
referenceSource: 'custom' as const,
referenceImage: {
type: 'data_url' as const,
dataUrl: referenceImageDataUrl,
},
country: 'NG',
consent: {
granted: true as const,
basis: 'explicit_consent',
scopes: ['biometric'],
},
}), [customerId, referenceImageDataUrl]);
if (!publicKey) return <Text>MaiGuard Verify is not configured.</Text>;
if (!consentGranted) return <Text>Biometric consent is required.</Text>;
const handleComplete = (result: FaceVerificationResult) => {
if (result.status === 'verified') onVerified(result);
// `onComplete` also receives review, rejection, high-risk, and pending outcomes.
// Keep those states distinct and apply your approved business workflow.
};
return (
<View style={{ flex: 1 }}>
<MaiGuardFaceVerification
config={{
apiKey: publicKey,
allowActiveChallengeLiveness: true,
}}
request={request}
photoEncoder={encodeCapturedPhoto}
onComplete={handleComplete}
onError={(error) => {
console.warn('MaiGuard Verify failed', error.code, error.message);
}}
onCancel={onClose}
/>
</View>
);
}EXPO_PUBLIC_* values are compiled into the application and are not secrets. Put only the MaiGuard public key there.
Country routing
Set request.country to the subject's ISO-2 country for non-Nigerian journeys (for example, country: 'GH'). The SDK normalizes explicit codes to uppercase and rejects empty, malformed, or conflicting subject jurisdictions before transport. If the country is omitted, a declared subject jurisdiction takes precedence, then a document issuing country; only an entirely unspecified jurisdiction keeps the legacy NG fallback. The providerLivenessHandler receives the resolved country. MaiGuard's API still determines whether the tenant and selected checks are available there; an SDK country value is not a coverage claim.
Reference portraits
MaiGuard does not dictate where the trusted portrait originates. It may come from BVN, NIN, a passport, a driver's licence, an internal profile, or a previous verified identity.
| Input | Use |
| --- | --- |
| string | An image data URL |
| { type: 'data_url', dataUrl } | An explicit image data URL |
| { type: 'uri', uri } | A local device image URI |
| { type: 'secure_url', url } | A short-lived HTTPS URL readable without privileged headers |
| { type: 'server_reference', referenceId } | An opaque reference resolved by your trusted verificationHandler |
For direct public-API verification, both the reference portrait and captured selfie are submitted inline:
- Use JPEG unless your verification policy requires another supported format.
- Resize images to approximately 720 pixels wide.
- Aim for 350 KB or less per encoded image.
- The total serialized verification request must remain below 1 MiB.
- The face should be front-facing, sharp, unobstructed, and large enough to identify.
If your reference image requires authorization headers or must never enter JavaScript, keep it on your backend and use server_reference with verificationHandler.
Production integration
Production liveness uses two boundaries:
providerLivenessHandleropens the approved native provider capture and returns provider-attested evidence.verificationHandleris required when the provider-selected live frame or reference portrait remains on your server.
1. Create the MaiGuard client
import {
MaiGuardFaceVerification,
VerifyApiClient,
VerifyError,
type FaceVerificationResult,
type ProviderLivenessHandler,
type VerificationHandler,
} from '@maiguard-hq/verify-react-native';
const verifyClient = new VerifyApiClient({
apiKey: process.env.EXPO_PUBLIC_MAIGUARD_VERIFY_KEY!,
});2. Bridge the approved provider adapter
createLivenessSession asks MaiGuard to select the provider configured for your tenant. Treat provider and params as opaque routing material and pass them only to the approved adapter.
const providerLivenessHandler: ProviderLivenessHandler = async ({
customerId,
verificationPurpose,
}) => {
const startedAt = Date.now();
const session = await verifyClient.createLivenessSession({
country: 'NG',
subject: { customerId, verificationPurpose },
});
const adapter = approvedLivenessAdapters[session.provider];
if (!adapter) {
throw new VerifyError(
`No approved adapter is installed for ${session.provider}.`,
'CHECK_UNAVAILABLE',
{ recoverable: false },
);
}
const capture = await adapter.start({
sessionId: session.sessionId,
params: session.params,
});
return {
sessionId: session.sessionId,
durationMs: Date.now() - startedAt,
// Include this only when the approved adapter returns the selected frame.
capturedImageDataUrl: capture.capturedImageDataUrl,
};
};approvedLivenessAdapters represents the provider package supplied or approved during MaiGuard onboarding. Provider credentials must be short-lived and obtained from your authenticated backend; never hardcode them in the application.
3. Add server-mediated verification when required
When the provider-selected frame or trusted reference stays on the server, submit the opaque references to your authenticated backend. The handler must return FaceVerificationResult.
const verificationHandler: VerificationHandler = async (submission) => {
const response = await fetch('https://your-api.example.com/identity/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${await getSignedInUserToken()}`,
},
body: JSON.stringify(submission),
});
if (!response.ok) throw new Error('Identity verification could not be completed.');
return response.json() as Promise<FaceVerificationResult>;
};Your backend must authenticate the user, validate that the customer and reference IDs belong to that user, resolve the provider-selected frame by livenessSessionId, call MaiGuard from the trusted server context, and return the normalized result. Do not accept arbitrary filesystem paths or unrestricted URLs from the application.
4. Render the production component
<MaiGuardFaceVerification
config={{
environment: 'live',
providerLivenessHandler,
verificationHandler,
}}
request={{
customerId,
verificationPurpose: 'account_recovery',
referenceSource: 'nin',
referenceImage: {
type: 'server_reference',
referenceId: authorityPortraitId,
},
country: 'NG',
consent: {
granted: true,
basis: 'explicit_consent',
scopes: ['biometric'],
},
}}
onComplete={(result) => {
if (result.status === 'verified') finishVerification(result);
else handleNonVerifiedDecision(result);
}}
onError={handleVerificationError}
onCancel={closeVerification}
/>If the provider adapter returns capturedImageDataUrl and the reference image is an inline data URL, you may omit verificationHandler and supply the public apiKey; the SDK will submit directly to MaiGuard. Server-held frames and server_reference always require verificationHandler.
Results and component lifecycle
onComplete runs for every completed API decision, not only successful verification.
type FaceVerificationResult = {
verificationId: string;
status: string;
confidenceScore: number | null;
riskLevel: string | null;
signals: string[];
checks: Array<{
check: string;
status: string;
confidence: number | null;
signals: string[];
}>;
liveness: {
status: 'pass' | 'fail' | 'not_attested';
assurance: 'active_challenge' | 'provider_attested';
challenges: Array<'blink' | 'turn'>;
durationMs: number;
};
};- Authorize the protected action only when your approved policy accepts the returned status and checks.
- A sandbox result has
liveness.status: 'not_attested'even when face matching succeeds. - The component displays its result until the host closes or unmounts it. Pass
onCancel; after completion it powers the Done button. - Use
onPhaseChangefor host analytics or navigation guards. Never log images or provider evidence. - Failed flows show Try again and reset to the idle state. Use
error.recoverableto decide whether the surrounding application should also offer another attempt.
Error handling
Every SDK failure is a VerifyError with code, message, recoverable, and an optional HTTP status.
| Code | Typical action |
| --- | --- |
| INVALID_CONFIG | Correct the key, URL, request, or adapter configuration |
| CONSENT_REQUIRED | Collect explicit biometric consent before mounting the flow |
| CAMERA_PERMISSION_DENIED | Explain how to enable camera permission in device settings |
| CAMERA_UNAVAILABLE | Ask the user to use a supported physical device |
| CHECK_UNAVAILABLE | Confirm production provider provisioning and native adapter setup |
| LIVENESS_FAILED | Allow a policy-approved retry or send the case for review |
| CAPTURE_FAILED | Retry capture after confirming the camera is available |
| CAPTURE_ENCODING_FAILED | Verify the file URI and custom photoEncoder |
| PAYLOAD_TOO_LARGE | Resize and compress both images before retrying |
| REQUEST_TIMEOUT | Check connectivity and retry when appropriate |
| AUTHENTICATION_FAILED | Check the public key and its environment |
| RATE_LIMITED | Wait before retrying |
| VERIFICATION_FAILED | Preserve the failed decision and follow your review policy |
| NETWORK_ERROR | Check connectivity without converting the failure into approval |
| UNKNOWN | Record non-biometric diagnostics and contact MaiGuard support |
Never convert an SDK, network, or provider failure into a fabricated verified result.
Headless API client
Use VerifyApiClient when you provide your own capture interface. The headless client accepts image data URLs; resolve local URIs before calling it.
const client = new VerifyApiClient({ apiKey: publicKey });
const result = await client.verifyFace(
{
customerId,
verificationPurpose: 'transaction',
referenceSource: 'previous_verification',
referenceImage: referenceImageDataUrl,
consent: { granted: true, basis: 'explicit_consent' },
},
{
faceImage: capturedImageDataUrl,
livenessSessionId,
assurance: 'provider_attested',
durationMs,
},
);Security and privacy boundary
- Green alignment means exactly one continuously tracked face is inside the oval with acceptable measured lighting.
- Face loss, multiple faces, or leaving the oval revokes alignment. A brief detector dropout during the instructed head turn is tolerated to avoid interrupting normal movement.
- Lighting is measured in the native camera pipeline. Android GPU-backed frames are never locked for JavaScript reading.
- Only the provider session can attest production liveness.
- The SDK keeps biometric images only in component memory for the active flow.
- Do not store reference portraits, selfies, or provider evidence in AsyncStorage, logs, analytics, or crash reports.
- Server-held portraits require an authenticated
verificationHandlerand ownership checks. - Only a verified server decision receives the green success treatment.
Troubleshooting
The app says a native module is missing or crashes immediately after installation
Rebuild the native application. Expo Go and a Metro-only reload cannot load the SDK's native modules.
A Worklets or frame-processor error appears
Confirm react-native-worklets-core/plugin is present, restart Metro with a cleared cache, and rebuild the application.
The camera is unavailable
Use a physical device, confirm camera permission is declared, and check that another application is not holding the camera.
The request is too large
Resize both images to approximately 720 pixels wide and reduce JPEG quality until each encoded image is around 350 KB or less.
A live key reports that provider liveness is required
Install and configure the approved production provider adapter, then pass providerLivenessHandler. The sandbox gesture flow cannot run with a live key.
The flow completes but the screen remains visible
Close or unmount the verification screen from onComplete, or pass onCancel and let the user press Done.
Public exports
MaiGuardFaceVerification— complete capture and result UI.VerifyApiClient— public-key transport for liveness-session bootstrap and headless face verification.VerifyError— stable SDK error type and codes.defaultPhotoEncoder— default local/HTTPS image-to-data-URL encoder.evaluateLivenessandinitialLivenessTracker— sandbox state-machine utilities.- Request, result, liveness-session, adapter, phase, photo-encoder, and theme types.
Support
- Documentation: https://docs.maiguard.com
- Support: [email protected]
License
MIT
