@jupitermetalabs/face-zk-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +181 -0
- package/assets/README.md +22 -0
- package/assets/face-guidance/face-logic.js.txt +77 -0
- package/assets/face-guidance/index.html +173 -0
- package/assets/face-guidance/pose-guidance.js.txt +403 -0
- package/assets/liveness/antispoof.js.txt +143 -0
- package/assets/liveness/index.html +451 -0
- package/assets/liveness/liveness.js.txt +1003 -0
- package/assets/mediapipe/face_mesh.js.txt +131 -0
- package/assets/mediapipe/face_mesh_solution_packed_assets.data +0 -0
- package/assets/mediapipe/face_mesh_solution_simd_wasm_bin.wasm +0 -0
- package/assets/mediapipe/face_mesh_solution_wasm_bin.wasm +0 -0
- package/assets/onnx/ort-wasm-simd.wasm +0 -0
- package/assets/onnx/ort-wasm.wasm +0 -0
- package/assets/onnx/ort.min.js.txt +7 -0
- package/assets/wasm/zk_face_wasm_bg.wasm +0 -0
- package/assets/zk-worker.html +472 -0
- package/cli/copy-ort-assets.js +65 -0
- package/cli/setup.js +266 -0
- package/dist/FaceZkSdk.d.ts +69 -0
- package/dist/FaceZkSdk.js +132 -0
- package/dist/assets/onnx/ort-min.d.ts +1 -0
- package/dist/assets/onnx/ort-min.js +8 -0
- package/dist/config/defaults.d.ts +49 -0
- package/dist/config/defaults.js +55 -0
- package/dist/config/types.d.ts +123 -0
- package/dist/config/types.js +16 -0
- package/dist/core/enrollment-core.d.ts +68 -0
- package/dist/core/enrollment-core.js +202 -0
- package/dist/core/matching.d.ts +69 -0
- package/dist/core/matching.js +96 -0
- package/dist/core/types.d.ts +365 -0
- package/dist/core/types.js +34 -0
- package/dist/core/verification-core.d.ts +120 -0
- package/dist/core/verification-core.js +434 -0
- package/dist/core/zk-core.d.ts +69 -0
- package/dist/core/zk-core.js +240 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +39 -0
- package/dist/react-native/adapters/faceEmbeddingProvider.d.ts +38 -0
- package/dist/react-native/adapters/faceEmbeddingProvider.js +41 -0
- package/dist/react-native/adapters/imageDataProvider.d.ts +53 -0
- package/dist/react-native/adapters/imageDataProvider.js +97 -0
- package/dist/react-native/adapters/livenessProvider.d.ts +133 -0
- package/dist/react-native/adapters/livenessProvider.js +144 -0
- package/dist/react-native/adapters/zkProofEngine-webview.d.ts +73 -0
- package/dist/react-native/adapters/zkProofEngine-webview.js +129 -0
- package/dist/react-native/components/FacePoseGuidanceWebView.d.ts +30 -0
- package/dist/react-native/components/FacePoseGuidanceWebView.js +474 -0
- package/dist/react-native/components/LivenessWebView.d.ts +39 -0
- package/dist/react-native/components/LivenessWebView.js +348 -0
- package/dist/react-native/components/OnnxRuntimeWebView.d.ts +54 -0
- package/dist/react-native/components/OnnxRuntimeWebView.js +394 -0
- package/dist/react-native/components/ZkProofWebView.d.ts +59 -0
- package/dist/react-native/components/ZkProofWebView.js +259 -0
- package/dist/react-native/dependencies.d.ts +144 -0
- package/dist/react-native/dependencies.js +123 -0
- package/dist/react-native/hooks/useOnnxLoader.d.ts +38 -0
- package/dist/react-native/hooks/useOnnxLoader.js +81 -0
- package/dist/react-native/hooks/useWasmLoader.d.ts +30 -0
- package/dist/react-native/hooks/useWasmLoader.js +122 -0
- package/dist/react-native/index.d.ts +59 -0
- package/dist/react-native/index.js +96 -0
- package/dist/react-native/services/FaceRecognition.d.ts +70 -0
- package/dist/react-native/services/FaceRecognition.js +517 -0
- package/dist/react-native/ui/FaceZkVerificationFlow.d.ts +97 -0
- package/dist/react-native/ui/FaceZkVerificationFlow.js +433 -0
- package/dist/react-native/ui/ReferenceEnrollmentFlow.d.ts +72 -0
- package/dist/react-native/ui/ReferenceEnrollmentFlow.js +321 -0
- package/dist/react-native/utils/faceAlignment.d.ts +37 -0
- package/dist/react-native/utils/faceAlignment.js +182 -0
- package/dist/react-native/utils/modelInitialisationChecks.d.ts +36 -0
- package/dist/react-native/utils/modelInitialisationChecks.js +92 -0
- package/dist/react-native/utils/resolveModelUri.d.ts +55 -0
- package/dist/react-native/utils/resolveModelUri.js +172 -0
- package/dist/react-native/utils/resolveUiConfig.d.ts +41 -0
- package/dist/react-native/utils/resolveUiConfig.js +76 -0
- package/dist/storage/defaultStorageAdapter.d.ts +44 -0
- package/dist/storage/defaultStorageAdapter.js +299 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/face-zk.config.example.js +88 -0
- package/package.json +76 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 JupiterMeta Labs
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { Asset } from "expo-asset";
|
|
17
|
+
import { useCameraPermissions } from "expo-camera";
|
|
18
|
+
import * as FileSystem from "expo-file-system/legacy";
|
|
19
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
20
|
+
import { ActivityIndicator, Image, Platform, StyleSheet, Text, TouchableOpacity, View, } from "react-native";
|
|
21
|
+
import { WebView } from "react-native-webview";
|
|
22
|
+
export const FacePoseGuidanceWebView = ({ referenceImageUri, onSuccess, onError, onCancel, manualTargetPose }) => {
|
|
23
|
+
const webViewRef = useRef(null);
|
|
24
|
+
const iframeRef = useRef(null);
|
|
25
|
+
const modelBase64Ref = useRef(null);
|
|
26
|
+
const [htmlContent, setHtmlContent] = useState(null);
|
|
27
|
+
const [permission, requestPermission] = useCameraPermissions();
|
|
28
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
29
|
+
const [step, setStep] = useState("LOADING");
|
|
30
|
+
const [targetPose, setTargetPose] = useState(null);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
loadResources();
|
|
33
|
+
// Web Message Listener
|
|
34
|
+
if (Platform.OS === "web") {
|
|
35
|
+
const messageHandler = (event) => {
|
|
36
|
+
try {
|
|
37
|
+
// Ensure message comes from our iframe/logic
|
|
38
|
+
if (typeof event.data === "string") {
|
|
39
|
+
// We need to parse checking if it's our format
|
|
40
|
+
// Our format: JSON string with { type: ... }
|
|
41
|
+
// But postMessage might send object if not stringified.
|
|
42
|
+
// The polyfill sends: window.parent.postMessage(msg, '*') where msg is the JSON string?
|
|
43
|
+
// Let's check polyfill below.
|
|
44
|
+
}
|
|
45
|
+
// If the data is already an object (common in window.postMessage), use it.
|
|
46
|
+
// If it's a string, parse it.
|
|
47
|
+
let data = event.data;
|
|
48
|
+
if (typeof data === "string") {
|
|
49
|
+
try {
|
|
50
|
+
data = JSON.parse(data);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
// Not our JSON message, ignore
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (data && data.type) {
|
|
58
|
+
handleWebMessage(data);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
console.error("Web Message Error:", e);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
window.addEventListener("message", messageHandler);
|
|
66
|
+
return () => window.removeEventListener("message", messageHandler);
|
|
67
|
+
}
|
|
68
|
+
}, []);
|
|
69
|
+
const handleWebMessage = (data) => {
|
|
70
|
+
console.log("[FacePoseGuidance Web] Message:", data.type);
|
|
71
|
+
if (data.type === "analysis_complete") {
|
|
72
|
+
setTargetPose(data.pose);
|
|
73
|
+
setStep("INSTRUCTION");
|
|
74
|
+
}
|
|
75
|
+
else if (data.type === "analysis_failed") {
|
|
76
|
+
onError("Reference analysis failed: " + data.message);
|
|
77
|
+
}
|
|
78
|
+
else if (data.type === "success") {
|
|
79
|
+
onSuccess(data.image, { targetPose, capturedPose: data.pose });
|
|
80
|
+
}
|
|
81
|
+
else if (data.type === "error") {
|
|
82
|
+
onError(data.message);
|
|
83
|
+
}
|
|
84
|
+
else if (data.type === "modelLoaded") {
|
|
85
|
+
console.log("Anti-spoof model loaded in WebView");
|
|
86
|
+
}
|
|
87
|
+
else if (data.type === "log") {
|
|
88
|
+
console.log("[WebView Log]", data.message);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const loadResources = async () => {
|
|
92
|
+
try {
|
|
93
|
+
console.log("[FacePoseGuidance] Loading resources...");
|
|
94
|
+
const htmlAsset = Asset.fromModule(require("../../assets/face-guidance/index.html"));
|
|
95
|
+
const jsAsset = Asset.fromModule(require("../../assets/face-guidance/pose-guidance.js.txt"));
|
|
96
|
+
const logicAsset = Asset.fromModule(require("../../assets/face-guidance/face-logic.js.txt"));
|
|
97
|
+
const antispoofAsset = Asset.fromModule(require("../../assets/liveness/antispoof.js.txt"));
|
|
98
|
+
const modelAsset = Asset.fromModule(require("../../assets/models/antispoof.onnx"));
|
|
99
|
+
await Promise.all([
|
|
100
|
+
htmlAsset.downloadAsync(),
|
|
101
|
+
jsAsset.downloadAsync(),
|
|
102
|
+
logicAsset.downloadAsync(),
|
|
103
|
+
antispoofAsset.downloadAsync(),
|
|
104
|
+
modelAsset.downloadAsync(),
|
|
105
|
+
]);
|
|
106
|
+
let html, jsContent, logicContent, antispoofContent, modelBase64;
|
|
107
|
+
if (Platform.OS === "web") {
|
|
108
|
+
// On Web, read from URI (fetched)
|
|
109
|
+
const htmlRes = await fetch(htmlAsset.uri);
|
|
110
|
+
html = await htmlRes.text();
|
|
111
|
+
const jsRes = await fetch(jsAsset.uri);
|
|
112
|
+
jsContent = await jsRes.text();
|
|
113
|
+
const logicRes = await fetch(logicAsset.uri);
|
|
114
|
+
logicContent = await logicRes.text();
|
|
115
|
+
const antispoofRes = await fetch(antispoofAsset.uri);
|
|
116
|
+
antispoofContent = await antispoofRes.text();
|
|
117
|
+
// On Web, we can load directly from URI, no need for Base64 injection which might be too large
|
|
118
|
+
// We will pass the URI to the WebView
|
|
119
|
+
modelBase64 = modelAsset.uri; // Reuse variable for URI on Web
|
|
120
|
+
// Inject Polyfill for ReactNativeWebView on Web
|
|
121
|
+
jsContent = `
|
|
122
|
+
${jsContent}
|
|
123
|
+
window.ReactNativeWebView = {
|
|
124
|
+
postMessage: function(data) {
|
|
125
|
+
window.parent.postMessage(data, '*');
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// On Native, read from local filesystem
|
|
132
|
+
html = await FileSystem.readAsStringAsync(htmlAsset.localUri || htmlAsset.uri);
|
|
133
|
+
jsContent = await FileSystem.readAsStringAsync(jsAsset.localUri || jsAsset.uri);
|
|
134
|
+
logicContent = await FileSystem.readAsStringAsync(logicAsset.localUri || logicAsset.uri);
|
|
135
|
+
antispoofContent = await FileSystem.readAsStringAsync(antispoofAsset.localUri || antispoofAsset.uri);
|
|
136
|
+
modelBase64 = await FileSystem.readAsStringAsync(modelAsset.localUri || modelAsset.uri, { encoding: FileSystem.EncodingType.Base64 });
|
|
137
|
+
}
|
|
138
|
+
// Inject JS into HTML
|
|
139
|
+
// Combine scripts and inject
|
|
140
|
+
const combinedScript = `
|
|
141
|
+
${logicContent}
|
|
142
|
+
${antispoofContent}
|
|
143
|
+
${jsContent}
|
|
144
|
+
`;
|
|
145
|
+
const finalHtml = html.replace('<script src="pose-guidance.js"></script>', `<script>${combinedScript}</script>`);
|
|
146
|
+
console.log("[FacePoseGuidance] HTML prepared");
|
|
147
|
+
setHtmlContent(finalHtml);
|
|
148
|
+
setStep("ANALYSIS");
|
|
149
|
+
modelBase64Ref.current = modelBase64;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
console.error("[FacePoseGuidance] Error loading resources:", error);
|
|
153
|
+
onError("Failed to load resources: " + error.message);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const handleMessage = (event) => {
|
|
157
|
+
if (!event)
|
|
158
|
+
return;
|
|
159
|
+
const data = event.nativeEvent ? event.nativeEvent.data : event.data;
|
|
160
|
+
try {
|
|
161
|
+
const parsed = typeof data === "string" ? JSON.parse(data) : data;
|
|
162
|
+
if (parsed.type === "analysis_complete") {
|
|
163
|
+
console.log("Analysis Complete", parsed.pose);
|
|
164
|
+
setTargetPose(parsed.pose);
|
|
165
|
+
setStep("INSTRUCTION");
|
|
166
|
+
}
|
|
167
|
+
else if (parsed.type === "success") {
|
|
168
|
+
onSuccess(parsed.image, { targetPose, capturedPose: parsed.pose });
|
|
169
|
+
}
|
|
170
|
+
else if (parsed.type === "error") {
|
|
171
|
+
console.error("WebView Error:", parsed.message);
|
|
172
|
+
onError(parsed.message);
|
|
173
|
+
}
|
|
174
|
+
else if (parsed.type === "modelLoaded") {
|
|
175
|
+
console.log("Anti-spoof model loaded in WebView");
|
|
176
|
+
}
|
|
177
|
+
else if (parsed.type === "log") {
|
|
178
|
+
console.log("[WebView Log]", parsed.message);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
console.log("Message parse error", e);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const injectScript = (script) => {
|
|
186
|
+
if (Platform.OS === "web") {
|
|
187
|
+
// @ts-expect-error — contentWindow.eval is present at runtime but not in the iframe element types
|
|
188
|
+
iframeRef.current?.contentWindow?.eval(script);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
webViewRef.current?.injectJavaScript(script);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const startAnalysis = async () => {
|
|
195
|
+
if (!referenceImageUri && !manualTargetPose)
|
|
196
|
+
return;
|
|
197
|
+
try {
|
|
198
|
+
let base64 = "";
|
|
199
|
+
if (referenceImageUri) {
|
|
200
|
+
if (Platform.OS === "web") {
|
|
201
|
+
base64 = referenceImageUri;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
base64 = await FileSystem.readAsStringAsync(referenceImageUri, {
|
|
205
|
+
encoding: FileSystem.EncodingType.Base64,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const script = `
|
|
210
|
+
window.startAnalysis("${base64}");
|
|
211
|
+
true;
|
|
212
|
+
`;
|
|
213
|
+
injectScript(script);
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
console.warn("Analysis start failed", e);
|
|
217
|
+
onError("Failed to read reference image");
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const startGuidance = async () => {
|
|
221
|
+
let currentPerm = permission;
|
|
222
|
+
if (!currentPerm?.granted) {
|
|
223
|
+
currentPerm = await requestPermission();
|
|
224
|
+
}
|
|
225
|
+
if (!currentPerm?.granted) {
|
|
226
|
+
onError("Camera permission is required to proceed.");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
setStep("GUIDANCE");
|
|
230
|
+
const script = `
|
|
231
|
+
window.startCamera();
|
|
232
|
+
true;
|
|
233
|
+
`;
|
|
234
|
+
injectScript(script);
|
|
235
|
+
};
|
|
236
|
+
const handleWebViewLoad = () => {
|
|
237
|
+
console.log("[FacePoseGuidance] WebView Loaded");
|
|
238
|
+
// Inject model data safely
|
|
239
|
+
if (modelBase64Ref.current) {
|
|
240
|
+
let injectModelScript;
|
|
241
|
+
if (Platform.OS === "web") {
|
|
242
|
+
// For Web, modelBase64Ref contains the URI
|
|
243
|
+
injectModelScript = `
|
|
244
|
+
if(window.loadAntispoofModelFromUrl) {
|
|
245
|
+
window.loadAntispoofModelFromUrl("${modelBase64Ref.current}");
|
|
246
|
+
}
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
// For Native, contains Base64
|
|
251
|
+
injectModelScript = `
|
|
252
|
+
if(window.loadAntispoofModel) {
|
|
253
|
+
window.loadAntispoofModel("${modelBase64Ref.current}");
|
|
254
|
+
}
|
|
255
|
+
`;
|
|
256
|
+
}
|
|
257
|
+
injectScript(injectModelScript);
|
|
258
|
+
}
|
|
259
|
+
if (step === "ANALYSIS") {
|
|
260
|
+
if (manualTargetPose) {
|
|
261
|
+
const script = `
|
|
262
|
+
(function() {
|
|
263
|
+
let attempts = 0;
|
|
264
|
+
const interval = setInterval(() => {
|
|
265
|
+
if (window.setTargetPose) {
|
|
266
|
+
clearInterval(interval);
|
|
267
|
+
window.setTargetPose(${manualTargetPose.yaw}, ${manualTargetPose.pitch}, ${manualTargetPose.roll});
|
|
268
|
+
} else {
|
|
269
|
+
attempts++;
|
|
270
|
+
if (attempts > 10) {
|
|
271
|
+
clearInterval(interval);
|
|
272
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'error', message: 'setTargetPose missing after timeout' }));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}, 500);
|
|
276
|
+
})();
|
|
277
|
+
true;
|
|
278
|
+
`;
|
|
279
|
+
injectScript(script);
|
|
280
|
+
}
|
|
281
|
+
else if (referenceImageUri) {
|
|
282
|
+
// Have an image, let's call our internal helper to base64 it and send it
|
|
283
|
+
startAnalysis();
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
// No reference and no manual target, skip analysis and go straight to instruction for straight-face
|
|
287
|
+
setTargetPose({ yaw: 0, pitch: 0, roll: 0 });
|
|
288
|
+
setStep("INSTRUCTION");
|
|
289
|
+
const script = `
|
|
290
|
+
(function() {
|
|
291
|
+
let attempts = 0;
|
|
292
|
+
const interval = setInterval(() => {
|
|
293
|
+
if (window.setTargetPose) {
|
|
294
|
+
clearInterval(interval);
|
|
295
|
+
window.setTargetPose(0, 0, 0);
|
|
296
|
+
} else {
|
|
297
|
+
attempts++;
|
|
298
|
+
if (attempts > 10) {
|
|
299
|
+
clearInterval(interval);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}, 500);
|
|
303
|
+
})();
|
|
304
|
+
true;
|
|
305
|
+
`;
|
|
306
|
+
injectScript(script);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
if (!htmlContent) {
|
|
311
|
+
return (<View style={styles.center}>
|
|
312
|
+
<ActivityIndicator size="large" color="#0000ff"/>
|
|
313
|
+
<Text style={styles.textLoading}>Loading resources...</Text>
|
|
314
|
+
</View>);
|
|
315
|
+
}
|
|
316
|
+
return (<View style={styles.container}>
|
|
317
|
+
{Platform.OS === "web" ? (<iframe ref={iframeRef} srcDoc={htmlContent} style={{ width: "100%", height: "100%", border: "none" }} onLoad={handleWebViewLoad}/>) : (<WebView ref={webViewRef} originWhitelist={["*"]}
|
|
318
|
+
// CRITICAL: baseUrl must be https://localhost/ to provide a Secure Context for WebAssembly/ONNX.
|
|
319
|
+
// It does NOT make actual network requests, but prevents the WebView from throwing security errors.
|
|
320
|
+
source={{ html: htmlContent, baseUrl: "https://localhost/" }} style={styles.webview} javaScriptEnabled={true} domStorageEnabled={true} allowsInlineMediaPlayback={true} mediaPlaybackRequiresUserAction={false} onMessage={handleMessage} onLoadEnd={handleWebViewLoad}
|
|
321
|
+
// @ts-expect-error — onPermissionRequest is an Android WebView prop not in react-native-webview types
|
|
322
|
+
onPermissionRequest={(event) => {
|
|
323
|
+
const { resources } = event.nativeEvent;
|
|
324
|
+
if (resources.includes("camera")) {
|
|
325
|
+
event.grant(resources);
|
|
326
|
+
}
|
|
327
|
+
}}/>)}
|
|
328
|
+
|
|
329
|
+
{/* Instruction Overlay */}
|
|
330
|
+
{step === "INSTRUCTION" && (<View style={styles.instructionOverlay}>
|
|
331
|
+
<View style={styles.instructionCard}>
|
|
332
|
+
<Text style={styles.instructionTitle}>
|
|
333
|
+
{referenceImageUri ? "Pose Matching" : "Face Enrollment"}
|
|
334
|
+
</Text>
|
|
335
|
+
<Text style={styles.instructionBody}>
|
|
336
|
+
{referenceImageUri
|
|
337
|
+
? "We need to match the pose in your reference image."
|
|
338
|
+
: "We will now capture your face. Please look straight into the camera."}
|
|
339
|
+
</Text>
|
|
340
|
+
|
|
341
|
+
<View style={styles.poseContainer}>
|
|
342
|
+
{referenceImageUri ? (<Image source={{ uri: referenceImageUri }} style={styles.referenceImage}/>) : null}
|
|
343
|
+
<Text style={styles.poseText}>
|
|
344
|
+
{targetPose?.yaw && Math.abs(targetPose.yaw) > 10
|
|
345
|
+
? `Look slightly ${targetPose.yaw > 0 ? "Right" : "Left"}`
|
|
346
|
+
: "Look Straight"}
|
|
347
|
+
</Text>
|
|
348
|
+
</View>
|
|
349
|
+
|
|
350
|
+
<Text style={styles.instructionSub}>
|
|
351
|
+
• Remove glasses • Ensure good lighting • Follow the arrows
|
|
352
|
+
</Text>
|
|
353
|
+
|
|
354
|
+
<TouchableOpacity style={styles.button} onPress={startGuidance}>
|
|
355
|
+
<Text style={styles.buttonText}>I'm Ready</Text>
|
|
356
|
+
</TouchableOpacity>
|
|
357
|
+
|
|
358
|
+
{onCancel && (<TouchableOpacity style={styles.cancelButton} onPress={onCancel}>
|
|
359
|
+
<Text style={styles.cancelText}>Go Back</Text>
|
|
360
|
+
</TouchableOpacity>)}
|
|
361
|
+
</View>
|
|
362
|
+
</View>)}
|
|
363
|
+
|
|
364
|
+
{/* Loading Overlay for Analysis phase */}
|
|
365
|
+
{step === "ANALYSIS" && (<View style={[
|
|
366
|
+
styles.instructionOverlay,
|
|
367
|
+
{ backgroundColor: "rgba(0,0,0,0.8)" },
|
|
368
|
+
]}>
|
|
369
|
+
<ActivityIndicator size="large" color="#6366f1"/>
|
|
370
|
+
<Text style={styles.textLoading}>
|
|
371
|
+
{referenceImageUri ? "Analyzing Reference Pose..." : "Initializing Camera Session..."}
|
|
372
|
+
</Text>
|
|
373
|
+
</View>)}
|
|
374
|
+
</View>);
|
|
375
|
+
};
|
|
376
|
+
const styles = StyleSheet.create({
|
|
377
|
+
container: {
|
|
378
|
+
flex: 1,
|
|
379
|
+
backgroundColor: "#000",
|
|
380
|
+
},
|
|
381
|
+
webview: {
|
|
382
|
+
flex: 1,
|
|
383
|
+
backgroundColor: "transparent",
|
|
384
|
+
},
|
|
385
|
+
center: {
|
|
386
|
+
flex: 1,
|
|
387
|
+
justifyContent: "center",
|
|
388
|
+
alignItems: "center",
|
|
389
|
+
backgroundColor: "#111",
|
|
390
|
+
},
|
|
391
|
+
textLoading: {
|
|
392
|
+
marginTop: 16,
|
|
393
|
+
color: "#ccc",
|
|
394
|
+
fontSize: 14,
|
|
395
|
+
},
|
|
396
|
+
instructionOverlay: {
|
|
397
|
+
...StyleSheet.absoluteFillObject,
|
|
398
|
+
backgroundColor: "rgba(0,0,0,0.85)",
|
|
399
|
+
justifyContent: "center",
|
|
400
|
+
alignItems: "center",
|
|
401
|
+
padding: 24,
|
|
402
|
+
},
|
|
403
|
+
instructionCard: {
|
|
404
|
+
width: "100%",
|
|
405
|
+
backgroundColor: "#1e1e1e",
|
|
406
|
+
borderRadius: 20,
|
|
407
|
+
padding: 24,
|
|
408
|
+
alignItems: "center",
|
|
409
|
+
borderWidth: 1,
|
|
410
|
+
borderColor: "#333",
|
|
411
|
+
},
|
|
412
|
+
instructionTitle: {
|
|
413
|
+
fontSize: 22,
|
|
414
|
+
fontWeight: "bold",
|
|
415
|
+
color: "#fff",
|
|
416
|
+
marginBottom: 12,
|
|
417
|
+
},
|
|
418
|
+
instructionBody: {
|
|
419
|
+
fontSize: 16,
|
|
420
|
+
color: "#aaa",
|
|
421
|
+
textAlign: "center",
|
|
422
|
+
marginBottom: 20,
|
|
423
|
+
},
|
|
424
|
+
poseContainer: {
|
|
425
|
+
alignItems: "center",
|
|
426
|
+
marginBottom: 24,
|
|
427
|
+
backgroundColor: "#000",
|
|
428
|
+
padding: 12,
|
|
429
|
+
borderRadius: 12,
|
|
430
|
+
},
|
|
431
|
+
referenceImage: {
|
|
432
|
+
width: 100,
|
|
433
|
+
height: 100,
|
|
434
|
+
borderRadius: 50,
|
|
435
|
+
marginBottom: 8,
|
|
436
|
+
borderWidth: 2,
|
|
437
|
+
borderColor: "#6366f1",
|
|
438
|
+
},
|
|
439
|
+
poseText: {
|
|
440
|
+
color: "#6366f1",
|
|
441
|
+
fontWeight: "bold",
|
|
442
|
+
fontSize: 16,
|
|
443
|
+
},
|
|
444
|
+
instructionSub: {
|
|
445
|
+
fontSize: 14,
|
|
446
|
+
color: "#777",
|
|
447
|
+
textAlign: "left",
|
|
448
|
+
width: "100%",
|
|
449
|
+
marginBottom: 24,
|
|
450
|
+
lineHeight: 22,
|
|
451
|
+
},
|
|
452
|
+
button: {
|
|
453
|
+
backgroundColor: "#6366f1",
|
|
454
|
+
paddingVertical: 14,
|
|
455
|
+
paddingHorizontal: 32,
|
|
456
|
+
borderRadius: 12,
|
|
457
|
+
width: "100%",
|
|
458
|
+
alignItems: "center",
|
|
459
|
+
},
|
|
460
|
+
buttonText: {
|
|
461
|
+
color: "#fff",
|
|
462
|
+
fontSize: 16,
|
|
463
|
+
fontWeight: "bold",
|
|
464
|
+
},
|
|
465
|
+
cancelButton: {
|
|
466
|
+
marginTop: 12,
|
|
467
|
+
padding: 10,
|
|
468
|
+
},
|
|
469
|
+
cancelText: {
|
|
470
|
+
color: "#aaa",
|
|
471
|
+
fontSize: 14,
|
|
472
|
+
textDecorationLine: "underline",
|
|
473
|
+
},
|
|
474
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 JupiterMeta Labs
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import React from "react";
|
|
17
|
+
export type LivenessPhase = "init" | "searching_far" | "recenter" | "challenge" | "move_closer" | "verifying_near" | "success" | "fail";
|
|
18
|
+
export type InstructionCode = "MOVE_BACK" | "CENTER_FACE" | "LOOK_STRAIGHT" | "BLINK" | "TURN_LEFT" | "TURN_RIGHT" | "MOVE_CLOSER" | "HOLD_PHONE_HIGHER" | "HOLD_PHONE_LOWER" | "HEAD_STRAIGHT" | "HOLD_STILL" | "VERIFYING" | "VERIFICATION_FAILED";
|
|
19
|
+
export interface LivenessState {
|
|
20
|
+
phase: LivenessPhase;
|
|
21
|
+
instructionCode: InstructionCode;
|
|
22
|
+
promptText: string;
|
|
23
|
+
progressPercent: number;
|
|
24
|
+
isFaceLocked: boolean;
|
|
25
|
+
icon: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ZkFaceAuthProps {
|
|
28
|
+
onSuccess: (imageUri: string, metadata?: any) => void;
|
|
29
|
+
onError: (message: string) => void;
|
|
30
|
+
manualTargetPose?: {
|
|
31
|
+
yaw: number;
|
|
32
|
+
pitch: number;
|
|
33
|
+
roll: number;
|
|
34
|
+
};
|
|
35
|
+
referenceImageUri?: string;
|
|
36
|
+
renderOverlay?: (state: LivenessState) => React.ReactNode;
|
|
37
|
+
headless?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare const ZkFaceAuth: React.FC<ZkFaceAuthProps>;
|