@jupitermetalabs/face-zk-sdk 0.3.4 → 0.3.7
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/README.md +46 -1
- package/assets/face-guidance/pose-guidance.js.txt +4 -2
- package/assets/liveness/liveness.js.txt +3 -0
- package/dist/FaceZkSdk.d.ts +69 -0
- package/dist/FaceZkSdk.js +136 -0
- package/dist/assets/face-guidance/pose-guidance.js.txt +4 -2
- package/dist/assets/liveness/liveness.js.txt +3 -0
- package/dist/assets/onnx/ort-min.d.ts +1 -0
- package/dist/assets/onnx/ort-min.js +11 -0
- package/dist/config/defaults.d.ts +51 -0
- package/dist/config/defaults.js +61 -0
- package/dist/config/types.d.ts +169 -0
- package/dist/config/types.js +17 -0
- package/dist/core/enrollment-core.d.ts +70 -0
- package/dist/core/enrollment-core.js +206 -0
- package/dist/core/idGenerator.d.ts +11 -0
- package/dist/core/idGenerator.js +32 -0
- package/dist/core/matching.d.ts +69 -0
- package/dist/core/matching.js +101 -0
- package/dist/core/types.d.ts +379 -0
- package/dist/core/types.js +37 -0
- package/dist/core/verification-core.d.ts +120 -0
- package/dist/core/verification-core.js +442 -0
- package/dist/core/zk-core.d.ts +69 -0
- package/dist/core/zk-core.js +244 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +51 -0
- package/dist/react-native/adapters/faceEmbeddingProvider.d.ts +38 -0
- package/dist/react-native/adapters/faceEmbeddingProvider.js +45 -0
- package/dist/react-native/adapters/imageDataProvider.d.ts +53 -0
- package/dist/react-native/adapters/imageDataProvider.js +134 -0
- package/dist/react-native/adapters/livenessProvider.d.ts +133 -0
- package/dist/react-native/adapters/livenessProvider.js +150 -0
- package/dist/react-native/adapters/zkProofEngine-webview.d.ts +73 -0
- package/dist/react-native/adapters/zkProofEngine-webview.js +135 -0
- package/dist/react-native/bundledRuntimeAssets.d.ts +39 -0
- package/dist/react-native/bundledRuntimeAssets.js +44 -0
- package/dist/react-native/components/FacePoseGuidanceWebView.d.ts +30 -0
- package/dist/react-native/components/FacePoseGuidanceWebView.js +530 -0
- package/dist/react-native/components/LivenessWebView.d.ts +39 -0
- package/dist/react-native/components/LivenessWebView.js +386 -0
- package/dist/react-native/components/OnnxRuntimeWebView.d.ts +58 -0
- package/dist/react-native/components/OnnxRuntimeWebView.js +518 -0
- package/dist/react-native/components/ZkProofWebView.d.ts +59 -0
- package/dist/react-native/components/ZkProofWebView.js +297 -0
- package/dist/react-native/dependencies.d.ts +144 -0
- package/dist/react-native/dependencies.js +130 -0
- package/dist/react-native/hooks/useOnnxLoader.d.ts +37 -0
- package/dist/react-native/hooks/useOnnxLoader.js +74 -0
- package/dist/react-native/hooks/useWasmLoader.d.ts +30 -0
- package/dist/react-native/hooks/useWasmLoader.js +158 -0
- package/dist/react-native/index.d.ts +61 -0
- package/dist/react-native/index.js +144 -0
- package/dist/react-native/services/FaceRecognition.d.ts +92 -0
- package/dist/react-native/services/FaceRecognition.js +674 -0
- package/dist/react-native/ui/FaceZkVerificationFlow.d.ts +97 -0
- package/dist/react-native/ui/FaceZkVerificationFlow.js +477 -0
- package/dist/react-native/ui/ReferenceEnrollmentFlow.d.ts +72 -0
- package/dist/react-native/ui/ReferenceEnrollmentFlow.js +369 -0
- package/dist/react-native/utils/faceAlignment.d.ts +37 -0
- package/dist/react-native/utils/faceAlignment.js +186 -0
- package/dist/react-native/utils/modelInitialisationChecks.d.ts +36 -0
- package/dist/react-native/utils/modelInitialisationChecks.js +128 -0
- package/dist/react-native/utils/resolveModelUri.d.ts +55 -0
- package/dist/react-native/utils/resolveModelUri.js +211 -0
- package/dist/react-native/utils/resolveRuntimeAsset.d.ts +25 -0
- package/dist/react-native/utils/resolveRuntimeAsset.js +94 -0
- package/dist/react-native/utils/resolveUiConfig.d.ts +41 -0
- package/dist/react-native/utils/resolveUiConfig.js +81 -0
- package/dist/storage/defaultStorageAdapter.d.ts +44 -0
- package/dist/storage/defaultStorageAdapter.js +344 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/face-zk.config.example.js +10 -3
- package/package.json +2 -2
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright 2026 JupiterMeta Labs
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.FacePoseGuidanceWebView = void 0;
|
|
52
|
+
const expo_camera_1 = require("expo-camera");
|
|
53
|
+
const FileSystem = __importStar(require("expo-file-system/legacy"));
|
|
54
|
+
const react_1 = __importStar(require("react"));
|
|
55
|
+
const react_native_1 = require("react-native");
|
|
56
|
+
const react_native_webview_1 = require("react-native-webview");
|
|
57
|
+
const FaceZkSdk_1 = require("../../FaceZkSdk");
|
|
58
|
+
const bundledRuntimeAssets_1 = require("../bundledRuntimeAssets");
|
|
59
|
+
const resolveModelUri_1 = require("../utils/resolveModelUri");
|
|
60
|
+
const resolveRuntimeAsset_1 = require("../utils/resolveRuntimeAsset");
|
|
61
|
+
const FacePoseGuidanceWebView = ({ referenceImageUri, onSuccess, onError, onCancel, manualTargetPose }) => {
|
|
62
|
+
const webViewRef = (0, react_1.useRef)(null);
|
|
63
|
+
const iframeRef = (0, react_1.useRef)(null);
|
|
64
|
+
const modelBase64Ref = (0, react_1.useRef)(null);
|
|
65
|
+
const [htmlContent, setHtmlContent] = (0, react_1.useState)(null);
|
|
66
|
+
const [permission, requestPermission] = (0, expo_camera_1.useCameraPermissions)();
|
|
67
|
+
const [isLoading, setIsLoading] = (0, react_1.useState)(true);
|
|
68
|
+
const [step, setStep] = (0, react_1.useState)("LOADING");
|
|
69
|
+
const [targetPose, setTargetPose] = (0, react_1.useState)(null);
|
|
70
|
+
(0, react_1.useEffect)(() => {
|
|
71
|
+
loadResources();
|
|
72
|
+
// Web Message Listener
|
|
73
|
+
if (react_native_1.Platform.OS === "web") {
|
|
74
|
+
const messageHandler = (event) => {
|
|
75
|
+
try {
|
|
76
|
+
// Ensure message comes from our iframe/logic
|
|
77
|
+
if (typeof event.data === "string") {
|
|
78
|
+
// We need to parse checking if it's our format
|
|
79
|
+
// Our format: JSON string with { type: ... }
|
|
80
|
+
// But postMessage might send object if not stringified.
|
|
81
|
+
// The polyfill sends: window.parent.postMessage(msg, '*') where msg is the JSON string?
|
|
82
|
+
// Let's check polyfill below.
|
|
83
|
+
}
|
|
84
|
+
// If the data is already an object (common in window.postMessage), use it.
|
|
85
|
+
// If it's a string, parse it.
|
|
86
|
+
let data = event.data;
|
|
87
|
+
if (typeof data === "string") {
|
|
88
|
+
try {
|
|
89
|
+
data = JSON.parse(data);
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
// Not our JSON message, ignore
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (data && data.type) {
|
|
97
|
+
handleWebMessage(data);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
console.error("Web Message Error:", e);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
window.addEventListener("message", messageHandler);
|
|
105
|
+
return () => window.removeEventListener("message", messageHandler);
|
|
106
|
+
}
|
|
107
|
+
}, []);
|
|
108
|
+
const handleWebMessage = (data) => {
|
|
109
|
+
console.log("[FacePoseGuidance Web] Message:", data.type);
|
|
110
|
+
if (data.type === "analysis_complete") {
|
|
111
|
+
setTargetPose(data.pose);
|
|
112
|
+
setStep("INSTRUCTION");
|
|
113
|
+
}
|
|
114
|
+
else if (data.type === "analysis_failed") {
|
|
115
|
+
onError("Reference analysis failed: " + data.message);
|
|
116
|
+
}
|
|
117
|
+
else if (data.type === "success") {
|
|
118
|
+
onSuccess(data.image, { targetPose, capturedPose: data.pose, faceMeshLandmarks: data.faceMeshLandmarks });
|
|
119
|
+
}
|
|
120
|
+
else if (data.type === "error") {
|
|
121
|
+
onError(data.message);
|
|
122
|
+
}
|
|
123
|
+
else if (data.type === "modelLoaded") {
|
|
124
|
+
console.log("Anti-spoof model loaded in WebView");
|
|
125
|
+
}
|
|
126
|
+
else if (data.type === "log") {
|
|
127
|
+
console.log("[WebView Log]", data.message);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const loadResources = async () => {
|
|
131
|
+
try {
|
|
132
|
+
console.log("[FacePoseGuidance] Loading resources...");
|
|
133
|
+
// antispoof model must be provided via initializeSdk({ models: { antispoof } })
|
|
134
|
+
if (!FaceZkSdk_1.FaceZkSdk.isInitialized()) {
|
|
135
|
+
throw new Error("[FaceZkSdk] SDK not initialized. Call initializeSdk() before using face guidance.\n" +
|
|
136
|
+
"Required: initializeSdk({ models: { detection, recognition, antispoof: { url: '...' } } })");
|
|
137
|
+
}
|
|
138
|
+
const sdkConfig = FaceZkSdk_1.FaceZkSdk.getConfig();
|
|
139
|
+
if (!sdkConfig.models.antispoof) {
|
|
140
|
+
throw new Error("[FaceZkSdk] models.antispoof is required for face pose guidance but was not provided.\n" +
|
|
141
|
+
"Add it to initializeSdk(): { models: { ..., antispoof: { url: 'https://...' } } }");
|
|
142
|
+
}
|
|
143
|
+
const allowedDomains = sdkConfig.allowedDomains;
|
|
144
|
+
// Pick runtime asset source: config override if provided, else bundled fallback.
|
|
145
|
+
// Used in the web branch where we need the URI before fetching text content.
|
|
146
|
+
const getRuntimeSource = (key) => sdkConfig.runtimeAssets?.[key] ?? bundledRuntimeAssets_1.BUNDLED_RUNTIME_ASSETS[key];
|
|
147
|
+
let html, jsContent, logicContent, antispoofContent, modelBase64;
|
|
148
|
+
if (react_native_1.Platform.OS === "web") {
|
|
149
|
+
// On Web: resolve each asset to a URI then fetch its text content.
|
|
150
|
+
// Uses getRuntimeSource so runtimeAssets config overrides apply on web too.
|
|
151
|
+
const [htmlUri, jsUri, logicUri, antispoofJsUri, modelUri] = await Promise.all([
|
|
152
|
+
(0, resolveModelUri_1.resolveModelUri)(getRuntimeSource('faceGuidanceHtml'), undefined, allowedDomains),
|
|
153
|
+
(0, resolveModelUri_1.resolveModelUri)(getRuntimeSource('faceGuidancePoseJs'), undefined, allowedDomains),
|
|
154
|
+
(0, resolveModelUri_1.resolveModelUri)(getRuntimeSource('faceGuidanceLogicJs'), undefined, allowedDomains),
|
|
155
|
+
(0, resolveModelUri_1.resolveModelUri)(getRuntimeSource('antispoofJs'), undefined, allowedDomains),
|
|
156
|
+
(0, resolveModelUri_1.resolveModelUri)(sdkConfig.models.antispoof, undefined, allowedDomains),
|
|
157
|
+
]);
|
|
158
|
+
const [htmlRes, jsRes, logicRes, antispoofRes] = await Promise.all([
|
|
159
|
+
fetch(htmlUri),
|
|
160
|
+
fetch(jsUri),
|
|
161
|
+
fetch(logicUri),
|
|
162
|
+
fetch(antispoofJsUri),
|
|
163
|
+
]);
|
|
164
|
+
html = await htmlRes.text();
|
|
165
|
+
jsContent = await jsRes.text();
|
|
166
|
+
logicContent = await logicRes.text();
|
|
167
|
+
antispoofContent = await antispoofRes.text();
|
|
168
|
+
// On Web, pass model URI directly (avoid large base64 payload)
|
|
169
|
+
modelBase64 = modelUri;
|
|
170
|
+
// Inject Polyfill for ReactNativeWebView on Web
|
|
171
|
+
jsContent = `
|
|
172
|
+
${jsContent}
|
|
173
|
+
window.ReactNativeWebView = {
|
|
174
|
+
postMessage: function(data) {
|
|
175
|
+
window.parent.postMessage(data, '*');
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
// On Native: resolveRuntimeAsset handles URI resolution + FileSystem read in one call
|
|
182
|
+
[html, jsContent, logicContent, antispoofContent] = await Promise.all([
|
|
183
|
+
(0, resolveRuntimeAsset_1.resolveRuntimeAsset)('faceGuidanceHtml', 'utf8', allowedDomains),
|
|
184
|
+
(0, resolveRuntimeAsset_1.resolveRuntimeAsset)('faceGuidancePoseJs', 'utf8', allowedDomains),
|
|
185
|
+
(0, resolveRuntimeAsset_1.resolveRuntimeAsset)('faceGuidanceLogicJs', 'utf8', allowedDomains),
|
|
186
|
+
(0, resolveRuntimeAsset_1.resolveRuntimeAsset)('antispoofJs', 'utf8', allowedDomains),
|
|
187
|
+
]);
|
|
188
|
+
const antispoofUri = await (0, resolveModelUri_1.resolveModelUri)(sdkConfig.models.antispoof, undefined, allowedDomains);
|
|
189
|
+
modelBase64 = await FileSystem.readAsStringAsync(antispoofUri, {
|
|
190
|
+
encoding: FileSystem.EncodingType.Base64,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// Inject JS into HTML
|
|
194
|
+
// Combine scripts and inject
|
|
195
|
+
const combinedScript = `
|
|
196
|
+
${logicContent}
|
|
197
|
+
${antispoofContent}
|
|
198
|
+
${jsContent}
|
|
199
|
+
`;
|
|
200
|
+
const finalHtml = html.replace('<script src="pose-guidance.js"></script>', `<script>${combinedScript}</script>`);
|
|
201
|
+
console.log("[FacePoseGuidance] HTML prepared");
|
|
202
|
+
setHtmlContent(finalHtml);
|
|
203
|
+
setStep("ANALYSIS");
|
|
204
|
+
modelBase64Ref.current = modelBase64;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
console.error("[FacePoseGuidance] Error loading resources:", error);
|
|
208
|
+
onError("Failed to load resources: " + error.message);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const handleMessage = (event) => {
|
|
212
|
+
if (!event)
|
|
213
|
+
return;
|
|
214
|
+
const data = event.nativeEvent ? event.nativeEvent.data : event.data;
|
|
215
|
+
try {
|
|
216
|
+
const parsed = typeof data === "string" ? JSON.parse(data) : data;
|
|
217
|
+
if (parsed.type === "analysis_complete") {
|
|
218
|
+
console.log("Analysis Complete", parsed.pose);
|
|
219
|
+
setTargetPose(parsed.pose);
|
|
220
|
+
setStep("INSTRUCTION");
|
|
221
|
+
}
|
|
222
|
+
else if (parsed.type === "success") {
|
|
223
|
+
onSuccess(parsed.image, { targetPose, capturedPose: parsed.pose, faceMeshLandmarks: parsed.faceMeshLandmarks });
|
|
224
|
+
}
|
|
225
|
+
else if (parsed.type === "error") {
|
|
226
|
+
console.error("WebView Error:", parsed.message);
|
|
227
|
+
onError(parsed.message);
|
|
228
|
+
}
|
|
229
|
+
else if (parsed.type === "modelLoaded") {
|
|
230
|
+
console.log("Anti-spoof model loaded in WebView");
|
|
231
|
+
}
|
|
232
|
+
else if (parsed.type === "log") {
|
|
233
|
+
console.log("[WebView Log]", parsed.message);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
console.log("Message parse error", e);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const injectScript = (script) => {
|
|
241
|
+
if (react_native_1.Platform.OS === "web") {
|
|
242
|
+
// @ts-expect-error — contentWindow.eval is present at runtime but not in the iframe element types
|
|
243
|
+
iframeRef.current?.contentWindow?.eval(script);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
webViewRef.current?.injectJavaScript(script);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
const startAnalysis = async () => {
|
|
250
|
+
if (!referenceImageUri && !manualTargetPose)
|
|
251
|
+
return;
|
|
252
|
+
try {
|
|
253
|
+
let base64 = "";
|
|
254
|
+
if (referenceImageUri) {
|
|
255
|
+
if (react_native_1.Platform.OS === "web") {
|
|
256
|
+
base64 = referenceImageUri;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
base64 = await FileSystem.readAsStringAsync(referenceImageUri, {
|
|
260
|
+
encoding: FileSystem.EncodingType.Base64,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const script = `
|
|
265
|
+
window.startAnalysis("${base64}");
|
|
266
|
+
true;
|
|
267
|
+
`;
|
|
268
|
+
injectScript(script);
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
console.warn("Analysis start failed", e);
|
|
272
|
+
onError("Failed to read reference image");
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const startGuidance = async () => {
|
|
276
|
+
let currentPerm = permission;
|
|
277
|
+
if (!currentPerm?.granted) {
|
|
278
|
+
currentPerm = await requestPermission();
|
|
279
|
+
}
|
|
280
|
+
if (!currentPerm?.granted) {
|
|
281
|
+
onError("Camera permission is required to proceed.");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
setStep("GUIDANCE");
|
|
285
|
+
const script = `
|
|
286
|
+
window.startCamera();
|
|
287
|
+
true;
|
|
288
|
+
`;
|
|
289
|
+
injectScript(script);
|
|
290
|
+
};
|
|
291
|
+
const handleWebViewLoad = () => {
|
|
292
|
+
console.log("[FacePoseGuidance] WebView Loaded");
|
|
293
|
+
// Inject model data safely
|
|
294
|
+
if (modelBase64Ref.current) {
|
|
295
|
+
let injectModelScript;
|
|
296
|
+
if (react_native_1.Platform.OS === "web") {
|
|
297
|
+
// For Web, modelBase64Ref contains the URI
|
|
298
|
+
injectModelScript = `
|
|
299
|
+
if(window.loadAntispoofModelFromUrl) {
|
|
300
|
+
window.loadAntispoofModelFromUrl("${modelBase64Ref.current}");
|
|
301
|
+
}
|
|
302
|
+
`;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
// For Native, contains Base64
|
|
306
|
+
injectModelScript = `
|
|
307
|
+
if(window.loadAntispoofModel) {
|
|
308
|
+
window.loadAntispoofModel("${modelBase64Ref.current}");
|
|
309
|
+
}
|
|
310
|
+
`;
|
|
311
|
+
}
|
|
312
|
+
injectScript(injectModelScript);
|
|
313
|
+
}
|
|
314
|
+
if (step === "ANALYSIS") {
|
|
315
|
+
if (manualTargetPose) {
|
|
316
|
+
const script = `
|
|
317
|
+
(function() {
|
|
318
|
+
let attempts = 0;
|
|
319
|
+
const interval = setInterval(() => {
|
|
320
|
+
if (window.setTargetPose) {
|
|
321
|
+
clearInterval(interval);
|
|
322
|
+
window.setTargetPose(${manualTargetPose.yaw}, ${manualTargetPose.pitch}, ${manualTargetPose.roll});
|
|
323
|
+
} else {
|
|
324
|
+
attempts++;
|
|
325
|
+
if (attempts > 10) {
|
|
326
|
+
clearInterval(interval);
|
|
327
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'error', message: 'setTargetPose missing after timeout' }));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}, 500);
|
|
331
|
+
})();
|
|
332
|
+
true;
|
|
333
|
+
`;
|
|
334
|
+
injectScript(script);
|
|
335
|
+
}
|
|
336
|
+
else if (referenceImageUri) {
|
|
337
|
+
// Have an image, let's call our internal helper to base64 it and send it
|
|
338
|
+
startAnalysis();
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
// No reference and no manual target, skip analysis and go straight to instruction for straight-face
|
|
342
|
+
setTargetPose({ yaw: 0, pitch: 0, roll: 0 });
|
|
343
|
+
setStep("INSTRUCTION");
|
|
344
|
+
const script = `
|
|
345
|
+
(function() {
|
|
346
|
+
let attempts = 0;
|
|
347
|
+
const interval = setInterval(() => {
|
|
348
|
+
if (window.setTargetPose) {
|
|
349
|
+
clearInterval(interval);
|
|
350
|
+
window.setTargetPose(0, 0, 0);
|
|
351
|
+
} else {
|
|
352
|
+
attempts++;
|
|
353
|
+
if (attempts > 10) {
|
|
354
|
+
clearInterval(interval);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}, 500);
|
|
358
|
+
})();
|
|
359
|
+
true;
|
|
360
|
+
`;
|
|
361
|
+
injectScript(script);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
if (!htmlContent) {
|
|
366
|
+
return (<react_native_1.View style={styles.center}>
|
|
367
|
+
<react_native_1.ActivityIndicator size="large" color="#0000ff"/>
|
|
368
|
+
<react_native_1.Text style={styles.textLoading}>Loading resources...</react_native_1.Text>
|
|
369
|
+
</react_native_1.View>);
|
|
370
|
+
}
|
|
371
|
+
return (<react_native_1.View style={styles.container}>
|
|
372
|
+
{react_native_1.Platform.OS === "web" ? (<iframe ref={iframeRef} srcDoc={htmlContent} style={{ width: "100%", height: "100%", border: "none" }} onLoad={handleWebViewLoad}/>) : (<react_native_webview_1.WebView ref={webViewRef} originWhitelist={["*"]}
|
|
373
|
+
// CRITICAL: baseUrl must be https://localhost/ to provide a Secure Context for WebAssembly/ONNX.
|
|
374
|
+
// It does NOT make actual network requests, but prevents the WebView from throwing security errors.
|
|
375
|
+
source={{ html: htmlContent, baseUrl: "https://localhost/" }} style={styles.webview} javaScriptEnabled={true} domStorageEnabled={true} allowsInlineMediaPlayback={true} mediaPlaybackRequiresUserAction={false} onMessage={handleMessage} onLoadEnd={handleWebViewLoad}
|
|
376
|
+
// @ts-expect-error — onPermissionRequest is an Android WebView prop not in react-native-webview types
|
|
377
|
+
onPermissionRequest={(event) => {
|
|
378
|
+
const { resources } = event.nativeEvent;
|
|
379
|
+
if (resources.includes("camera")) {
|
|
380
|
+
event.grant(resources);
|
|
381
|
+
}
|
|
382
|
+
}}/>)}
|
|
383
|
+
|
|
384
|
+
{/* Instruction Overlay */}
|
|
385
|
+
{step === "INSTRUCTION" && (<react_native_1.View style={styles.instructionOverlay}>
|
|
386
|
+
<react_native_1.View style={styles.instructionCard}>
|
|
387
|
+
<react_native_1.Text style={styles.instructionTitle}>
|
|
388
|
+
{referenceImageUri ? "Pose Matching" : "Face Enrollment"}
|
|
389
|
+
</react_native_1.Text>
|
|
390
|
+
<react_native_1.Text style={styles.instructionBody}>
|
|
391
|
+
{referenceImageUri
|
|
392
|
+
? "We need to match the pose in your reference image."
|
|
393
|
+
: "We will now capture your face. Please look straight into the camera."}
|
|
394
|
+
</react_native_1.Text>
|
|
395
|
+
|
|
396
|
+
<react_native_1.View style={styles.poseContainer}>
|
|
397
|
+
{referenceImageUri ? (<react_native_1.Image source={{ uri: referenceImageUri }} style={styles.referenceImage}/>) : null}
|
|
398
|
+
<react_native_1.Text style={styles.poseText}>
|
|
399
|
+
{targetPose?.yaw && Math.abs(targetPose.yaw) > 10
|
|
400
|
+
? `Look slightly ${targetPose.yaw > 0 ? "Left" : "Right"}`
|
|
401
|
+
: "Look Straight"}
|
|
402
|
+
</react_native_1.Text>
|
|
403
|
+
</react_native_1.View>
|
|
404
|
+
|
|
405
|
+
<react_native_1.Text style={styles.instructionSub}>
|
|
406
|
+
• Remove glasses • Ensure good lighting • Follow the arrows
|
|
407
|
+
</react_native_1.Text>
|
|
408
|
+
|
|
409
|
+
<react_native_1.TouchableOpacity style={styles.button} onPress={startGuidance}>
|
|
410
|
+
<react_native_1.Text style={styles.buttonText}>I'm Ready</react_native_1.Text>
|
|
411
|
+
</react_native_1.TouchableOpacity>
|
|
412
|
+
|
|
413
|
+
{onCancel && (<react_native_1.TouchableOpacity style={styles.cancelButton} onPress={onCancel}>
|
|
414
|
+
<react_native_1.Text style={styles.cancelText}>Go Back</react_native_1.Text>
|
|
415
|
+
</react_native_1.TouchableOpacity>)}
|
|
416
|
+
</react_native_1.View>
|
|
417
|
+
</react_native_1.View>)}
|
|
418
|
+
|
|
419
|
+
{/* Loading Overlay for Analysis phase */}
|
|
420
|
+
{step === "ANALYSIS" && (<react_native_1.View style={[
|
|
421
|
+
styles.instructionOverlay,
|
|
422
|
+
{ backgroundColor: "rgba(0,0,0,0.8)" },
|
|
423
|
+
]}>
|
|
424
|
+
<react_native_1.ActivityIndicator size="large" color="#6366f1"/>
|
|
425
|
+
<react_native_1.Text style={styles.textLoading}>
|
|
426
|
+
{referenceImageUri ? "Analyzing Reference Pose..." : "Initializing Camera Session..."}
|
|
427
|
+
</react_native_1.Text>
|
|
428
|
+
</react_native_1.View>)}
|
|
429
|
+
</react_native_1.View>);
|
|
430
|
+
};
|
|
431
|
+
exports.FacePoseGuidanceWebView = FacePoseGuidanceWebView;
|
|
432
|
+
const styles = react_native_1.StyleSheet.create({
|
|
433
|
+
container: {
|
|
434
|
+
flex: 1,
|
|
435
|
+
backgroundColor: "#000",
|
|
436
|
+
},
|
|
437
|
+
webview: {
|
|
438
|
+
flex: 1,
|
|
439
|
+
backgroundColor: "transparent",
|
|
440
|
+
},
|
|
441
|
+
center: {
|
|
442
|
+
flex: 1,
|
|
443
|
+
justifyContent: "center",
|
|
444
|
+
alignItems: "center",
|
|
445
|
+
backgroundColor: "#111",
|
|
446
|
+
},
|
|
447
|
+
textLoading: {
|
|
448
|
+
marginTop: 16,
|
|
449
|
+
color: "#ccc",
|
|
450
|
+
fontSize: 14,
|
|
451
|
+
},
|
|
452
|
+
instructionOverlay: {
|
|
453
|
+
...react_native_1.StyleSheet.absoluteFillObject,
|
|
454
|
+
backgroundColor: "rgba(0,0,0,0.85)",
|
|
455
|
+
justifyContent: "center",
|
|
456
|
+
alignItems: "center",
|
|
457
|
+
padding: 24,
|
|
458
|
+
},
|
|
459
|
+
instructionCard: {
|
|
460
|
+
width: "100%",
|
|
461
|
+
backgroundColor: "#1e1e1e",
|
|
462
|
+
borderRadius: 20,
|
|
463
|
+
padding: 24,
|
|
464
|
+
alignItems: "center",
|
|
465
|
+
borderWidth: 1,
|
|
466
|
+
borderColor: "#333",
|
|
467
|
+
},
|
|
468
|
+
instructionTitle: {
|
|
469
|
+
fontSize: 22,
|
|
470
|
+
fontWeight: "bold",
|
|
471
|
+
color: "#fff",
|
|
472
|
+
marginBottom: 12,
|
|
473
|
+
},
|
|
474
|
+
instructionBody: {
|
|
475
|
+
fontSize: 16,
|
|
476
|
+
color: "#aaa",
|
|
477
|
+
textAlign: "center",
|
|
478
|
+
marginBottom: 20,
|
|
479
|
+
},
|
|
480
|
+
poseContainer: {
|
|
481
|
+
alignItems: "center",
|
|
482
|
+
marginBottom: 24,
|
|
483
|
+
backgroundColor: "#000",
|
|
484
|
+
padding: 12,
|
|
485
|
+
borderRadius: 12,
|
|
486
|
+
},
|
|
487
|
+
referenceImage: {
|
|
488
|
+
width: 100,
|
|
489
|
+
height: 100,
|
|
490
|
+
borderRadius: 50,
|
|
491
|
+
marginBottom: 8,
|
|
492
|
+
borderWidth: 2,
|
|
493
|
+
borderColor: "#6366f1",
|
|
494
|
+
},
|
|
495
|
+
poseText: {
|
|
496
|
+
color: "#6366f1",
|
|
497
|
+
fontWeight: "bold",
|
|
498
|
+
fontSize: 16,
|
|
499
|
+
},
|
|
500
|
+
instructionSub: {
|
|
501
|
+
fontSize: 14,
|
|
502
|
+
color: "#777",
|
|
503
|
+
textAlign: "left",
|
|
504
|
+
width: "100%",
|
|
505
|
+
marginBottom: 24,
|
|
506
|
+
lineHeight: 22,
|
|
507
|
+
},
|
|
508
|
+
button: {
|
|
509
|
+
backgroundColor: "#6366f1",
|
|
510
|
+
paddingVertical: 14,
|
|
511
|
+
paddingHorizontal: 32,
|
|
512
|
+
borderRadius: 12,
|
|
513
|
+
width: "100%",
|
|
514
|
+
alignItems: "center",
|
|
515
|
+
},
|
|
516
|
+
buttonText: {
|
|
517
|
+
color: "#fff",
|
|
518
|
+
fontSize: 16,
|
|
519
|
+
fontWeight: "bold",
|
|
520
|
+
},
|
|
521
|
+
cancelButton: {
|
|
522
|
+
marginTop: 12,
|
|
523
|
+
padding: 10,
|
|
524
|
+
},
|
|
525
|
+
cancelText: {
|
|
526
|
+
color: "#aaa",
|
|
527
|
+
fontSize: 14,
|
|
528
|
+
textDecorationLine: "underline",
|
|
529
|
+
},
|
|
530
|
+
});
|
|
@@ -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>;
|