@myazahq/kyc-sdk-react-native 2.6.0 → 3.0.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/README.md +128 -4
- package/android/build.gradle +30 -16
- package/android/consumer-rules.pro +22 -0
- package/android/src/main/AndroidManifest.xml +17 -6
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +27 -72
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +14 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/MlKitModelReadiness.kt +154 -0
- package/app.plugin.js +57 -2
- package/ios/HybridMyazaTextRecognizer.swift +8 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +21 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +8 -0
- package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +16 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +31 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +2 -0
- package/package.json +17 -8
- package/src/MyazaKYC.tsx +4 -11
- package/src/assets/fonts/Karla_400Regular.ttf +0 -0
- package/src/assets/fonts/Karla_500Medium.ttf +0 -0
- package/src/assets/fonts/Karla_600SemiBold.ttf +0 -0
- package/src/assets/fonts/Karla_700Bold.ttf +0 -0
- package/src/assets/fonts/OFL-Karla.txt +93 -0
- package/src/assets/fonts/OFL-SpaceGrotesk.txt +93 -0
- package/src/assets/fonts/README.md +12 -0
- package/src/assets/fonts/SpaceGrotesk_500Medium.ttf +0 -0
- package/src/assets/fonts/SpaceGrotesk_600SemiBold.ttf +0 -0
- package/src/assets/fonts/SpaceGrotesk_700Bold.ttf +0 -0
- package/src/capture/useAutoCapture.ts +8 -1
- package/src/components/DocumentCropper.tsx +30 -24
- package/src/components/DocumentReview.tsx +11 -1
- package/src/components/DocumentReviewSide.tsx +6 -2
- package/src/components/DocumentReviewZoom.tsx +5 -1
- package/src/components/Icon.tsx +8 -7
- package/src/components/KycFlow.tsx +9 -2
- package/src/components/MyazaButton.tsx +9 -0
- package/src/components/RequiredDocumentPill.tsx +114 -0
- package/src/components/StepHeader.tsx +23 -3
- package/src/components/documentReviewCopy.ts +39 -0
- package/src/components/fonts.ts +14 -17
- package/src/components/stepHeaderMeta.tsx +6 -1
- package/src/config/documentCaptureMethods.ts +31 -0
- package/src/config/workflowMerge.ts +2 -0
- package/src/index.ts +5 -10
- package/src/lib/documentCaptureCheck.ts +136 -0
- package/src/lib/model-ready.ts +82 -0
- package/src/lib/prime-models.ts +50 -0
- package/src/lib/resubmit.ts +36 -7
- package/src/lib/selfie-sharpness.ts +211 -0
- package/src/liveness/avatarSource.ts +59 -0
- package/src/liveness/useModelReady.ts +9 -55
- package/src/mrz/extract.ts +71 -4
- package/src/mrz/textRecognizer.ts +36 -0
- package/src/mrz/useTextModelReady.ts +21 -0
- package/src/screens/DocumentCaptureStep.tsx +177 -93
- package/src/screens/LivenessAvatar.tsx +18 -13
- package/src/screens/LivenessStep.tsx +37 -0
- package/src/screens/MrzScanView.tsx +33 -0
- package/src/screens/document/CaptureCheckNotice.tsx +77 -0
- package/src/screens/document/UploadPhase.tsx +177 -0
- package/src/screens/document/useDocumentCamera.ts +109 -0
- package/src/screens/liveness/LivenessOutcome.tsx +4 -1
- package/src/screens/liveness/SelfiePreview.tsx +19 -4
- package/src/services/api-types.ts +28 -2
- package/src/services/api.ts +19 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/mediaCompress.ts +23 -5
- package/src/specs/MyazaTextRecognizer.nitro.ts +29 -0
- package/src/store/kycStore.ts +8 -2
- package/src/types/config.ts +15 -0
- package/src/MyazaBiometricAuth.tsx +0 -119
- package/src/assets/liveness/Blink.gif +0 -0
- package/src/assets/liveness/Nod.gif +0 -0
- package/src/assets/liveness/Smile.gif +0 -0
- package/src/assets/liveness/Turn.gif +0 -0
- package/src/lib/biometric-auth.ts +0 -52
- package/src/screens/biometric/BiometricAuthFlow.tsx +0 -170
- package/src/services/api-biometric.ts +0 -37
- package/src/services/api-types-biometric.ts +0 -41
|
@@ -1,65 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { useNativeModelReady, type ModelReadyState } from '../lib/model-ready';
|
|
3
2
|
import { isFaceModelReady, primeFaceModel } from './visionCameraFaceDetector';
|
|
4
3
|
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// it, which keeps ~8 MB per device out of the APK but leaves a window where
|
|
9
|
-
// detection cannot run: first launch before the download lands, or a device with
|
|
10
|
-
// no Google Play Services at all.
|
|
11
|
-
//
|
|
12
|
-
// This has to be answered BEFORE the camera opens. `detectFace` reports through
|
|
13
|
-
// `FaceResult`, where a missing model and an empty frame are both
|
|
14
|
-
// `faceCount: 0` — so gating on the per-frame result would strand the user on
|
|
15
|
-
// "position your face" indefinitely, with the SDK unable to say why. That is the
|
|
16
|
-
// single failure mode this whole gate exists to prevent.
|
|
17
|
-
//
|
|
18
|
-
// iOS is always ready (Apple Vision is a system framework), so this resolves on
|
|
19
|
-
// the first tick there and costs nothing.
|
|
20
|
-
|
|
21
|
-
export type ModelReadyState = 'ready' | 'preparing' | 'unavailable';
|
|
4
|
+
// The face half of the shared readiness gate. The rule, the polling and the
|
|
5
|
+
// reasoning live in ../lib/model-ready.ts, because the text recogniser needs
|
|
6
|
+
// exactly the same thing and one decision should not exist in two places.
|
|
22
7
|
|
|
23
|
-
|
|
24
|
-
const MODEL_WAIT_MS = 20_000;
|
|
25
|
-
/** Gap between readiness polls while the download is in flight. */
|
|
26
|
-
const POLL_INTERVAL_MS = 500;
|
|
8
|
+
export type { ModelReadyState };
|
|
27
9
|
|
|
28
10
|
/**
|
|
29
11
|
* Tracks whether on-device face detection can run.
|
|
30
12
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* The flow primes the download at open (see `MyazaKYC.tsx`), so by the time the
|
|
36
|
-
* user reaches liveness this is normally already `'ready'` and no waiting screen
|
|
37
|
-
* is ever shown.
|
|
13
|
+
* `'preparing'` while Play Services fetches the model, `'ready'` once it can
|
|
14
|
+
* run, `'unavailable'` when it could not be obtained — no Play Services, no
|
|
15
|
+
* network, or a declined install.
|
|
38
16
|
*/
|
|
39
17
|
export function useFaceModelReady(): ModelReadyState {
|
|
40
|
-
|
|
41
|
-
isFaceModelReady() ? 'ready' : 'preparing',
|
|
42
|
-
);
|
|
43
|
-
const startedAt = useRef(Date.now());
|
|
44
|
-
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
if (state !== 'preparing') return;
|
|
47
|
-
|
|
48
|
-
// Re-prime rather than assume the open-time call ran: the step can be
|
|
49
|
-
// reached directly in a resumed flow, and prepareModel() is a no-op once
|
|
50
|
-
// the model is present.
|
|
51
|
-
primeFaceModel();
|
|
52
|
-
|
|
53
|
-
const id = setInterval(() => {
|
|
54
|
-
if (isFaceModelReady()) {
|
|
55
|
-
setState('ready');
|
|
56
|
-
} else if (Date.now() - startedAt.current > MODEL_WAIT_MS) {
|
|
57
|
-
setState('unavailable');
|
|
58
|
-
}
|
|
59
|
-
}, POLL_INTERVAL_MS);
|
|
60
|
-
|
|
61
|
-
return () => clearInterval(id);
|
|
62
|
-
}, [state]);
|
|
63
|
-
|
|
64
|
-
return state;
|
|
18
|
+
return useNativeModelReady(isFaceModelReady, primeFaceModel);
|
|
65
19
|
}
|
package/src/mrz/extract.ts
CHANGED
|
@@ -57,6 +57,15 @@ function fit(line: string, width: number): string | null {
|
|
|
57
57
|
if (line.length >= width - 2 && line.length < width) return line.padEnd(width, '<');
|
|
58
58
|
// Long by a stray glyph or three: drop the trailing noise.
|
|
59
59
|
if (line.length > width && line.length <= width + 3) return line.slice(0, width);
|
|
60
|
+
// A filler run the recogniser miscounted. Android returns runs of '<' as
|
|
61
|
+
// guillemets whose number does not match the fillers printed, so the mapped
|
|
62
|
+
// run overshoots or falls short by many characters (a passport's first line
|
|
63
|
+
// came back 53 wide on the Flutter SDK, 2026-09-15). Trailing fillers are
|
|
64
|
+
// padding and carry no data, so only they are removed or added.
|
|
65
|
+
if (line.length > width && !/[^<]/.test(line.slice(width))) return line.slice(0, width);
|
|
66
|
+
if (line.length < width && line.length >= Math.floor(width / 2) && line.endsWith('<')) {
|
|
67
|
+
return line.padEnd(width, '<');
|
|
68
|
+
}
|
|
60
69
|
return null;
|
|
61
70
|
}
|
|
62
71
|
|
|
@@ -65,10 +74,9 @@ function fit(line: string, width: number): string | null {
|
|
|
65
74
|
* not carry a complete, valid one.
|
|
66
75
|
*/
|
|
67
76
|
export function extractMrz(recognizedLines: string[], now: Date = new Date()): MrzScan | null {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (candidates.length === 0) return null;
|
|
77
|
+
const pieces = recognizedLines.map(sanitizeMrzLine).filter((line) => line.length > 0);
|
|
78
|
+
const candidates = pieces.filter(looksLikeMrzLine);
|
|
79
|
+
if (candidates.length === 0) return fromFragments(pieces, now);
|
|
72
80
|
|
|
73
81
|
// 1) A single line already holding the whole MRZ (some recognisers merge).
|
|
74
82
|
for (const line of candidates) {
|
|
@@ -97,5 +105,64 @@ export function extractMrz(recognizedLines: string[], now: Date = new Date()): M
|
|
|
97
105
|
if (scan) return scan;
|
|
98
106
|
}
|
|
99
107
|
|
|
108
|
+
return fromFragments(pieces, now);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Split lines.
|
|
113
|
+
//
|
|
114
|
+
// On a high-resolution still, Android's recogniser can return ONE printed MRZ
|
|
115
|
+
// line as two text lines (seen on the Flutter SDK, 2026-09-15: a passport's
|
|
116
|
+
// two-line band came back as three lines). Joining runs of adjacent pieces back
|
|
117
|
+
// to line width recovers it; a wrong join is harmless, as above. Mirrored in
|
|
118
|
+
// kyc-sdk-flutter's mrz_extract.dart.
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
/** The longest run of pieces one printed line is ever split into. */
|
|
122
|
+
const MAX_PIECES_PER_LINE = 4;
|
|
123
|
+
|
|
124
|
+
interface JoinedRow {
|
|
125
|
+
end: number;
|
|
126
|
+
text: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Joined rows of `width`, keyed by the index of the piece each one starts at. */
|
|
130
|
+
function rowsByStart(pieces: string[], width: number): Map<number, JoinedRow[]> {
|
|
131
|
+
const rows = new Map<number, JoinedRow[]>();
|
|
132
|
+
for (let i = 0; i < pieces.length; i++) {
|
|
133
|
+
let text = '';
|
|
134
|
+
for (let j = i; j < pieces.length && j < i + MAX_PIECES_PER_LINE; j++) {
|
|
135
|
+
text += pieces[j]!;
|
|
136
|
+
// Generous: a joined row may carry an over-counted filler run `fit` trims.
|
|
137
|
+
if (text.length > width * 2) break;
|
|
138
|
+
const fitted = fit(text, width);
|
|
139
|
+
if (fitted) rows.set(i, [...(rows.get(i) ?? []), { end: j, text: fitted }]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return rows;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Consecutive joined rows of line width: TD3 two of 44, TD1 three of 30. */
|
|
146
|
+
function fromFragments(pieces: string[], now: Date): MrzScan | null {
|
|
147
|
+
const td3 = rowsByStart(pieces, 44);
|
|
148
|
+
for (const list of td3.values()) {
|
|
149
|
+
for (const a of list) {
|
|
150
|
+
for (const b of td3.get(a.end + 1) ?? []) {
|
|
151
|
+
const scan = parseMrz(a.text + b.text, now);
|
|
152
|
+
if (scan) return scan;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const td1 = rowsByStart(pieces, 30);
|
|
157
|
+
for (const list of td1.values()) {
|
|
158
|
+
for (const a of list) {
|
|
159
|
+
for (const b of td1.get(a.end + 1) ?? []) {
|
|
160
|
+
for (const c of td1.get(b.end + 1) ?? []) {
|
|
161
|
+
const scan = parseMrz(a.text + b.text + c.text, now);
|
|
162
|
+
if (scan) return scan;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
100
167
|
return null;
|
|
101
168
|
}
|
|
@@ -71,6 +71,42 @@ export function hasTextRecognizer(): boolean {
|
|
|
71
71
|
return resolve() !== null;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Start fetching the text model, so it is warm by the time the document step
|
|
76
|
+
* runs.
|
|
77
|
+
*
|
|
78
|
+
* Android fetches ML Kit's models through Play Services rather than bundling
|
|
79
|
+
* them, which keeps ~18.5 MB per device out of the APK but leaves a window on
|
|
80
|
+
* first launch where recognition cannot work. Priming at flow start puts that
|
|
81
|
+
* download behind the consent and ID-type screens instead of in front of the
|
|
82
|
+
* camera.
|
|
83
|
+
*
|
|
84
|
+
* Best-effort and non-blocking, exactly like `primeFaceModel()`: a failure here
|
|
85
|
+
* is not fatal, because {@link isTextModelReady} is what the document step
|
|
86
|
+
* actually reads. iOS resolves immediately (Apple Vision is a system
|
|
87
|
+
* framework).
|
|
88
|
+
*/
|
|
89
|
+
export function primeTextModel(): void {
|
|
90
|
+
resolve()?.prepareModel().catch(() => {
|
|
91
|
+
/* best-effort: isTextModelReady() is the real signal */
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether text recognition can run right now.
|
|
97
|
+
*
|
|
98
|
+
* `recognizeText` cannot answer this — a missing model and a frame with no text
|
|
99
|
+
* are both an empty `lines` array — so auto-capture would silently never fire
|
|
100
|
+
* and the MRZ would never produce a chip key, with nothing able to say why.
|
|
101
|
+
*
|
|
102
|
+
* `false` when the native module is absent entirely (Expo Go), because in that
|
|
103
|
+
* build there is no recogniser to become ready. Callers treat both the same
|
|
104
|
+
* way: fall back to the manual shutter, which was always the primary route.
|
|
105
|
+
*/
|
|
106
|
+
export function isTextModelReady(): boolean {
|
|
107
|
+
return resolve()?.isModelReady() ?? false;
|
|
108
|
+
}
|
|
109
|
+
|
|
74
110
|
/**
|
|
75
111
|
* The bottom slice of the frame the MRZ occupies.
|
|
76
112
|
*
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useNativeModelReady, type ModelReadyState } from '../lib/model-ready';
|
|
2
|
+
import { isTextModelReady, primeTextModel } from './textRecognizer';
|
|
3
|
+
|
|
4
|
+
// The text half of the shared readiness gate. The rule, the polling and the
|
|
5
|
+
// reasoning live in ../lib/model-ready.ts — the same gate the face detector
|
|
6
|
+
// uses, for the same reason.
|
|
7
|
+
|
|
8
|
+
export type { ModelReadyState };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Tracks whether on-device text recognition can run.
|
|
12
|
+
*
|
|
13
|
+
* The live MRZ scanner is what gates on this: the printed strip IS the chip's
|
|
14
|
+
* access key, so a recogniser that can never run leaves the user aiming at a
|
|
15
|
+
* passport that is silently never read. Auto-capture deliberately does NOT gate
|
|
16
|
+
* on it — there the recogniser is an accelerator and the manual shutter is
|
|
17
|
+
* always live, so an absent model costs convenience rather than a dead end.
|
|
18
|
+
*/
|
|
19
|
+
export function useTextModelReady(): ModelReadyState {
|
|
20
|
+
return useNativeModelReady(isTextModelReady, primeTextModel);
|
|
21
|
+
}
|