@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.
Files changed (80) hide show
  1. package/README.md +128 -4
  2. package/android/build.gradle +30 -16
  3. package/android/consumer-rules.pro +22 -0
  4. package/android/src/main/AndroidManifest.xml +17 -6
  5. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +27 -72
  6. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +14 -0
  7. package/android/src/main/java/com/margelo/nitro/myazakyc/MlKitModelReadiness.kt +154 -0
  8. package/app.plugin.js +57 -2
  9. package/ios/HybridMyazaTextRecognizer.swift +8 -0
  10. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +21 -0
  11. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +2 -0
  12. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +8 -0
  13. package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +16 -0
  14. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +2 -0
  15. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +31 -0
  16. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +2 -0
  17. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +2 -0
  18. package/package.json +17 -8
  19. package/src/MyazaKYC.tsx +4 -11
  20. package/src/assets/fonts/Karla_400Regular.ttf +0 -0
  21. package/src/assets/fonts/Karla_500Medium.ttf +0 -0
  22. package/src/assets/fonts/Karla_600SemiBold.ttf +0 -0
  23. package/src/assets/fonts/Karla_700Bold.ttf +0 -0
  24. package/src/assets/fonts/OFL-Karla.txt +93 -0
  25. package/src/assets/fonts/OFL-SpaceGrotesk.txt +93 -0
  26. package/src/assets/fonts/README.md +12 -0
  27. package/src/assets/fonts/SpaceGrotesk_500Medium.ttf +0 -0
  28. package/src/assets/fonts/SpaceGrotesk_600SemiBold.ttf +0 -0
  29. package/src/assets/fonts/SpaceGrotesk_700Bold.ttf +0 -0
  30. package/src/capture/useAutoCapture.ts +8 -1
  31. package/src/components/DocumentCropper.tsx +30 -24
  32. package/src/components/DocumentReview.tsx +11 -1
  33. package/src/components/DocumentReviewSide.tsx +6 -2
  34. package/src/components/DocumentReviewZoom.tsx +5 -1
  35. package/src/components/Icon.tsx +8 -7
  36. package/src/components/KycFlow.tsx +9 -2
  37. package/src/components/MyazaButton.tsx +9 -0
  38. package/src/components/RequiredDocumentPill.tsx +114 -0
  39. package/src/components/StepHeader.tsx +23 -3
  40. package/src/components/documentReviewCopy.ts +39 -0
  41. package/src/components/fonts.ts +14 -17
  42. package/src/components/stepHeaderMeta.tsx +6 -1
  43. package/src/config/documentCaptureMethods.ts +31 -0
  44. package/src/config/workflowMerge.ts +2 -0
  45. package/src/index.ts +5 -10
  46. package/src/lib/documentCaptureCheck.ts +136 -0
  47. package/src/lib/model-ready.ts +82 -0
  48. package/src/lib/prime-models.ts +50 -0
  49. package/src/lib/resubmit.ts +36 -7
  50. package/src/lib/selfie-sharpness.ts +211 -0
  51. package/src/liveness/avatarSource.ts +59 -0
  52. package/src/liveness/useModelReady.ts +9 -55
  53. package/src/mrz/extract.ts +71 -4
  54. package/src/mrz/textRecognizer.ts +36 -0
  55. package/src/mrz/useTextModelReady.ts +21 -0
  56. package/src/screens/DocumentCaptureStep.tsx +177 -93
  57. package/src/screens/LivenessAvatar.tsx +18 -13
  58. package/src/screens/LivenessStep.tsx +37 -0
  59. package/src/screens/MrzScanView.tsx +33 -0
  60. package/src/screens/document/CaptureCheckNotice.tsx +77 -0
  61. package/src/screens/document/UploadPhase.tsx +177 -0
  62. package/src/screens/document/useDocumentCamera.ts +109 -0
  63. package/src/screens/liveness/LivenessOutcome.tsx +4 -1
  64. package/src/screens/liveness/SelfiePreview.tsx +19 -4
  65. package/src/services/api-types.ts +28 -2
  66. package/src/services/api.ts +19 -3
  67. package/src/services/deviceMetadata.ts +1 -1
  68. package/src/services/mediaCompress.ts +23 -5
  69. package/src/specs/MyazaTextRecognizer.nitro.ts +29 -0
  70. package/src/store/kycStore.ts +8 -2
  71. package/src/types/config.ts +15 -0
  72. package/src/MyazaBiometricAuth.tsx +0 -119
  73. package/src/assets/liveness/Blink.gif +0 -0
  74. package/src/assets/liveness/Nod.gif +0 -0
  75. package/src/assets/liveness/Smile.gif +0 -0
  76. package/src/assets/liveness/Turn.gif +0 -0
  77. package/src/lib/biometric-auth.ts +0 -52
  78. package/src/screens/biometric/BiometricAuthFlow.tsx +0 -170
  79. package/src/services/api-biometric.ts +0 -37
  80. package/src/services/api-types-biometric.ts +0 -41
@@ -1,65 +1,19 @@
1
- import { useEffect, useRef, useState } from 'react';
2
-
1
+ import { useNativeModelReady, type ModelReadyState } from '../lib/model-ready';
3
2
  import { isFaceModelReady, primeFaceModel } from './visionCameraFaceDetector';
4
3
 
5
- // ─── Face-model readiness gate ────────────────────────────────────────────────
6
- //
7
- // Android fetches ML Kit's face model through Play Services rather than bundling
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
- /** How long to wait for the model before calling it unavailable. */
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
- * Returns `'preparing'` while Play Services fetches the model, `'ready'` once it
32
- * can run, and `'unavailable'` when it could not be obtained within
33
- * {@link MODEL_WAIT_MS} — no Play Services, no network, or a declined install.
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
- const [state, setState] = useState<ModelReadyState>(() =>
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
  }
@@ -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 candidates = recognizedLines
69
- .map(sanitizeMrzLine)
70
- .filter(looksLikeMrzLine);
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
+ }