@myazahq/kyc-sdk-react-native 2.5.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 +277 -7
- 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 -70
- 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 +146 -10
- package/ios/HybridMyazaFaceDetector.swift +8 -3
- 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 +30 -9
- 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/CountryField.tsx +8 -0
- package/src/components/CountryRegionPicker.tsx +184 -0
- package/src/components/DialCodePicker.tsx +47 -68
- package/src/components/DialCodeRow.tsx +102 -0
- 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/FramedMapPicker.tsx +122 -0
- package/src/components/GeoBadge.tsx +34 -0
- package/src/components/Icon.tsx +10 -192
- package/src/components/KycFlow.tsx +69 -34
- package/src/components/LineSkeleton.tsx +100 -0
- package/src/components/MapChrome.tsx +67 -0
- package/src/components/MapPinMarker.tsx +54 -0
- package/src/components/MapPinPicker.tsx +179 -0
- package/src/components/MilestoneTrack.tsx +155 -0
- package/src/components/MyazaButton.tsx +9 -0
- package/src/components/MyazaInput.tsx +17 -1
- package/src/components/OptionRow.tsx +22 -3
- package/src/components/PhoneNumberInput.tsx +8 -0
- package/src/components/PoweredBy.tsx +5 -5
- package/src/components/PresenceBlocks.tsx +153 -0
- package/src/components/RequiredDocumentPill.tsx +114 -0
- package/src/components/StepHeader.tsx +23 -3
- package/src/components/StepView.tsx +17 -0
- package/src/components/StickyActions.tsx +56 -0
- package/src/components/dialCodeRows.ts +64 -0
- package/src/components/documentReviewCopy.ts +39 -0
- package/src/components/fonts.ts +14 -17
- package/src/components/icon-map.ts +176 -0
- package/src/components/stepHeaderMeta.tsx +48 -5
- package/src/config/addressCollection.ts +125 -0
- package/src/config/biometricOptions.ts +100 -0
- package/src/config/business.ts +20 -1
- package/src/config/businessSteps.ts +8 -2
- package/src/config/consentStep.ts +19 -0
- package/src/config/documentCaptureMethods.ts +31 -0
- package/src/config/proofOfAddress.ts +54 -11
- package/src/config/regions.ts +24 -0
- package/src/config/stepOrder.ts +79 -5
- package/src/config/uploadLimits.ts +35 -0
- package/src/config/workflowMerge.ts +13 -5
- package/src/index.ts +49 -1
- package/src/lib/address-current-location.ts +171 -0
- package/src/lib/address-field-modes.ts +119 -0
- package/src/lib/address-flow.ts +172 -0
- package/src/lib/address-helpers.ts +48 -0
- package/src/lib/address-line.ts +76 -0
- package/src/lib/address-pin-move.ts +97 -0
- package/src/lib/address-step-recovery.ts +63 -0
- package/src/lib/authed-image.ts +78 -0
- package/src/lib/biometric-copy.ts +51 -0
- package/src/lib/captureRing.ts +83 -0
- package/src/lib/country-adoption.ts +89 -0
- package/src/lib/documentCaptureCheck.ts +136 -0
- package/src/lib/inferred-country.ts +65 -0
- package/src/lib/livenessLayout.ts +49 -0
- package/src/lib/map-frame.ts +159 -0
- package/src/lib/map-tiles.ts +143 -0
- package/src/lib/model-ready.ts +82 -0
- package/src/lib/poa-country-gate.ts +27 -0
- package/src/lib/prime-models.ts +50 -0
- package/src/lib/resubmit.ts +36 -7
- package/src/lib/result-copy.ts +116 -0
- package/src/lib/result-wait.ts +53 -0
- package/src/lib/review-map-surface.ts +26 -0
- package/src/lib/scope.ts +31 -0
- package/src/lib/selfie-sharpness.ts +211 -0
- package/src/lib/selfie-upload-wait.ts +79 -0
- package/src/lib/street-view-fov.ts +42 -0
- package/src/lib/webview-available.ts +47 -0
- package/src/liveness/avatarSource.ts +59 -0
- package/src/liveness/useLiveness.ts +6 -1
- 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/presence/background-math.ts +100 -0
- package/src/presence/background-store.ts +82 -0
- package/src/presence/background.ts +165 -0
- package/src/presence/foreground-service.ts +193 -0
- package/src/presence/fs.ts +51 -0
- package/src/presence/geofence.ts +41 -0
- package/src/presence/math.ts +44 -0
- package/src/presence/post.ts +40 -0
- package/src/presence/report.ts +87 -0
- package/src/presence/sampler.ts +110 -0
- package/src/presence/status.ts +92 -0
- package/src/presence/store.ts +94 -0
- package/src/presence/tier.ts +36 -0
- package/src/presence/watch-wait.ts +81 -0
- package/src/screens/AddressCountryControl.tsx +115 -0
- package/src/screens/BusinessDetailsFields.tsx +3 -0
- package/src/screens/BusinessDetailsStep.tsx +2 -0
- package/src/screens/BusinessDocumentSlot.tsx +3 -2
- package/src/screens/BusinessDocumentsStep.tsx +4 -55
- package/src/screens/CompanyInfoFields.tsx +6 -1
- package/src/screens/ConsentStep.tsx +6 -17
- package/src/screens/ContactActions.tsx +54 -0
- package/src/screens/ContactDestinationField.tsx +4 -0
- package/src/screens/ContactEntryPanel.tsx +75 -0
- package/src/screens/ContactVerificationStep.tsx +26 -56
- package/src/screens/CountrySelectStep.tsx +13 -147
- package/src/screens/DocumentCaptureStep.tsx +177 -93
- package/src/screens/LivenessAvatar.tsx +33 -16
- package/src/screens/LivenessStep.tsx +188 -49
- package/src/screens/MrzScanView.tsx +33 -0
- package/src/screens/PoaDocumentTypeList.tsx +66 -0
- package/src/screens/ProofOfAddressParts.tsx +151 -0
- package/src/screens/ProofOfAddressStep.tsx +58 -99
- package/src/screens/SubmittedBadge.tsx +25 -0
- package/src/screens/SubmittedError.tsx +64 -0
- package/src/screens/SubmittedResult.tsx +117 -0
- package/src/screens/SubmittedStep.tsx +71 -164
- package/src/screens/SubmittedSuccess.tsx +127 -0
- package/src/screens/SubmittedWaiting.tsx +45 -0
- package/src/screens/address/AddressEntranceStep.tsx +161 -0
- package/src/screens/address/AddressIntroGate.tsx +148 -0
- package/src/screens/address/AddressMapStub.tsx +50 -0
- package/src/screens/address/AddressPinStep.tsx +178 -0
- package/src/screens/address/AddressReviewStep.tsx +162 -0
- package/src/screens/address/AddressSandboxTabs.tsx +158 -0
- package/src/screens/address/AddressSearchField.tsx +115 -0
- package/src/screens/address/AddressSearchStep.tsx +94 -0
- package/src/screens/address/CurrentLocationRow.tsx +135 -0
- package/src/screens/address/DetailsSheet.tsx +175 -0
- package/src/screens/address/DetailsSheetFields.tsx +184 -0
- package/src/screens/address/EntranceDropzone.tsx +178 -0
- package/src/screens/address/EntranceFraming.tsx +66 -0
- package/src/screens/address/EntrancePills.tsx +60 -0
- package/src/screens/address/FramedStreetView.tsx +155 -0
- package/src/screens/address/IntroDisclosures.tsx +210 -0
- package/src/screens/address/LabelDecisionRow.tsx +113 -0
- package/src/screens/address/PinSummaryRow.tsx +105 -0
- package/src/screens/address/ReviewAddressBand.tsx +111 -0
- package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
- package/src/screens/address/ReviewMapPicture.tsx +106 -0
- package/src/screens/address/SearchResults.tsx +105 -0
- package/src/screens/address/SearchScreen.tsx +176 -0
- package/src/screens/address/SkipForNow.tsx +43 -0
- package/src/screens/address/StreetViewChrome.tsx +81 -0
- package/src/screens/address/detail-values.ts +22 -0
- package/src/screens/address/fix-source.ts +27 -0
- package/src/screens/address/index.ts +8 -0
- package/src/screens/address/meta.ts +44 -0
- package/src/screens/address/use-address-flow.ts +200 -0
- package/src/screens/address/use-label-pin.ts +80 -0
- package/src/screens/address/use-pin-actions.ts +192 -0
- package/src/screens/consent/model.ts +79 -10
- 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/CaptureRing.tsx +91 -0
- package/src/screens/liveness/LivenessCamera.tsx +63 -0
- package/src/screens/liveness/LivenessHandover.tsx +43 -0
- package/src/screens/liveness/LivenessOutcome.tsx +8 -4
- package/src/screens/liveness/SelfiePreview.tsx +31 -7
- package/src/screens/liveness/ShutterFlash.tsx +25 -0
- package/src/screens/liveness/index.ts +4 -0
- package/src/screens/liveness/useSelfieUpload.ts +23 -3
- package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
- package/src/screens/useAddressPhotoAttach.ts +77 -0
- package/src/screens/useBusinessDocumentAttach.ts +93 -0
- package/src/screens/usePoaAttach.ts +12 -15
- package/src/services/api-types.ts +119 -1
- package/src/services/api-verify-types.ts +37 -0
- package/src/services/api.ts +149 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/location.ts +234 -0
- package/src/services/mediaCompress.ts +23 -5
- package/src/services/uploadErrors.ts +30 -0
- package/src/services/workflowGate.ts +7 -0
- package/src/specs/MyazaTextRecognizer.nitro.ts +29 -0
- package/src/store/address.ts +97 -0
- package/src/store/derive.ts +29 -4
- package/src/store/kycStore.ts +120 -5
- package/src/store/serverConfig.ts +18 -0
- package/src/store/session.ts +23 -4
- package/src/store/state.ts +134 -0
- package/src/store/submit.ts +18 -1
- package/src/types/config.ts +63 -1
- package/src/types/verification.ts +12 -0
- package/src/types/workflow.ts +97 -1
- 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/README.md
CHANGED
|
@@ -56,8 +56,11 @@ npx expo install @myazahq/kyc-sdk-react-native \
|
|
|
56
56
|
react-native-safe-area-context react-native-svg
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
Add the config plugins to `app.json`. The SDK's plugin adds the iOS camera
|
|
60
|
-
|
|
59
|
+
Add the config plugins to `app.json`. The SDK's plugin adds the iOS camera and
|
|
60
|
+
location usage strings + Android `CAMERA`/`INTERNET` and foreground location
|
|
61
|
+
permissions (location backs the address-collection step's "Use my current
|
|
62
|
+
location" and presence attestation; `["@myazahq/kyc-sdk-react-native",
|
|
63
|
+
{ "location": false }]` opts out); the VisionCamera plugin wires up
|
|
61
64
|
the camera + frame processors. Make sure the **New Architecture** is on (it is by
|
|
62
65
|
default on Expo SDK 56):
|
|
63
66
|
|
|
@@ -67,13 +70,18 @@ default on Expo SDK 56):
|
|
|
67
70
|
"expo": {
|
|
68
71
|
"newArchEnabled": true,
|
|
69
72
|
"plugins": [
|
|
70
|
-
["react-native-vision-camera", { "enableMicrophonePermission": false }],
|
|
71
73
|
"@myazahq/kyc-sdk-react-native"
|
|
72
74
|
]
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
```
|
|
76
78
|
|
|
79
|
+
VisionCamera v5 ships **no config plugin** (v4 did), so it takes no `plugins`
|
|
80
|
+
entry. Listing it makes `expo prebuild` load the package's main entry as a
|
|
81
|
+
plugin and fail with `Cannot find module '.../lib/VisionCamera'`. The camera
|
|
82
|
+
permission and usage strings come from this SDK's own plugin; pass
|
|
83
|
+
`cameraPermission` to change the iOS wording.
|
|
84
|
+
|
|
77
85
|
Then build a dev client (regenerates the native projects):
|
|
78
86
|
|
|
79
87
|
```sh
|
|
@@ -82,8 +90,64 @@ npx expo run:ios # iOS
|
|
|
82
90
|
JAVA_HOME=/path/to/jdk-17 npx expo run:android # Android — needs JDK 17
|
|
83
91
|
```
|
|
84
92
|
|
|
85
|
-
> The SDK plugin accepts
|
|
86
|
-
> `["@myazahq/kyc-sdk-react-native", { "cameraPermission": "Your message…"
|
|
93
|
+
> The SDK plugin accepts optional custom prompts:
|
|
94
|
+
> `["@myazahq/kyc-sdk-react-native", { "cameraPermission": "Your message…",
|
|
95
|
+
> "locationPermission": "Your message…", "nfcPermission": "Your message…" }]`.
|
|
96
|
+
|
|
97
|
+
#### NFC is opt-in
|
|
98
|
+
|
|
99
|
+
Reading the eMRTD chip in a passport or chip ID card needs platform permissions
|
|
100
|
+
that most apps should not carry, so the SDK declares **none of them** unless you
|
|
101
|
+
ask:
|
|
102
|
+
|
|
103
|
+
```jsonc
|
|
104
|
+
["@myazahq/kyc-sdk-react-native", { "nfc": true }]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
With it on, the plugin writes the Android `NFC` permission plus a
|
|
108
|
+
`uses-feature android:required="false"` declaration, and on iOS the
|
|
109
|
+
`NFCReaderUsageDescription`, the reader-session entitlement and the eMRTD
|
|
110
|
+
application identifier.
|
|
111
|
+
|
|
112
|
+
Leave it off and none of that is added. The chip step already checks for a radio
|
|
113
|
+
at runtime and skips itself when there is none, so an app built without NFC
|
|
114
|
+
behaves exactly like a phone that has no NFC hardware — every other step is
|
|
115
|
+
unaffected.
|
|
116
|
+
|
|
117
|
+
Two reasons this is opt-in rather than on by default:
|
|
118
|
+
|
|
119
|
+
- **iOS code signing.** The reader entitlement requires the App ID to carry the
|
|
120
|
+
"NFC Tag Reading" capability in the Apple Developer portal. Adding it
|
|
121
|
+
unconditionally breaks signing for every consumer who does not read chips and
|
|
122
|
+
has not enabled that capability.
|
|
123
|
+
- **Play Store visibility.** `android.permission.NFC` makes Android infer that
|
|
124
|
+
the app *requires* an NFC radio, which hides it from every device without one.
|
|
125
|
+
That is why the permission and the `uses-feature` declaration are written
|
|
126
|
+
together and never separately.
|
|
127
|
+
|
|
128
|
+
> **Changed in 3.0.0.** Earlier versions declared the Android NFC permission in
|
|
129
|
+
> the library manifest, so it merged into every host app regardless of this
|
|
130
|
+
> option — the opt-in only ever governed iOS. If you read chips on Android and
|
|
131
|
+
> have not set `nfc: true`, set it now: without it the permission is no longer
|
|
132
|
+
> declared and the chip step will find no radio. Apps that never read chips need
|
|
133
|
+
> no change and lose a permission they never wanted.
|
|
134
|
+
|
|
135
|
+
### Optional modules
|
|
136
|
+
|
|
137
|
+
Four `expo-*` modules are **optional peers**. The SDK loads each one lazily and
|
|
138
|
+
carries on without it, so nothing crashes if you skip them — but each one is
|
|
139
|
+
missing a capability rather than a detail, so install them deliberately:
|
|
140
|
+
|
|
141
|
+
| Module | What installing it buys | Without it |
|
|
142
|
+
|--------|------------------------|-----------|
|
|
143
|
+
| `expo-device` | Make, model, manufacturer and physical-vs-simulator in the device metadata | Those fields are omitted and the device class is guessed from the platform, so Device Intelligence has a weaker fingerprint and shared-device detection suffers |
|
|
144
|
+
| `expo-application` | Your app's id, version and build number on the submission | The `app` block is omitted entirely, so a result cannot be traced to the build that produced it |
|
|
145
|
+
| `expo-localization` | The device's region, explicitly | Country defaults and the reported locale fall back to the JS runtime's locale, which often carries no region at all (`en` rather than `en-NG`) |
|
|
146
|
+
| `expo-document-picker` | "Choose a file" on the proof-of-address and KYB document steps | Those steps accept a camera capture only, so a PDF bank statement cannot be submitted at all |
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
npx expo install expo-device expo-application expo-localization expo-document-picker
|
|
150
|
+
```
|
|
87
151
|
|
|
88
152
|
### Bare React Native app (no Expo prebuild)
|
|
89
153
|
|
|
@@ -100,7 +164,8 @@ npm install @myazahq/kyc-sdk-react-native \
|
|
|
100
164
|
react-native-worklets react-native-nitro-modules react-native-nitro-image \
|
|
101
165
|
react-native-safe-area-context react-native-svg \
|
|
102
166
|
expo expo-image-manipulator expo-image-picker expo-speech expo-font \
|
|
103
|
-
expo-glass-effect expo-
|
|
167
|
+
expo-glass-effect expo-crypto \
|
|
168
|
+
expo-location
|
|
104
169
|
|
|
105
170
|
# 3. iOS pods:
|
|
106
171
|
cd ios && pod install && cd ..
|
|
@@ -113,11 +178,15 @@ prebuild):
|
|
|
113
178
|
```xml
|
|
114
179
|
<key>NSCameraUsageDescription</key>
|
|
115
180
|
<string>We use the camera to photograph your ID and capture a live selfie.</string>
|
|
181
|
+
<key>NSLocationWhenInUseUsageDescription</key>
|
|
182
|
+
<string>We use your location to confirm you are at the address you pin during verification.</string>
|
|
116
183
|
```
|
|
117
184
|
- **Android** — add to `android/app/src/main/AndroidManifest.xml`:
|
|
118
185
|
```xml
|
|
119
186
|
<uses-permission android:name="android.permission.CAMERA" />
|
|
120
187
|
<uses-permission android:name="android.permission.INTERNET" />
|
|
188
|
+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
189
|
+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
121
190
|
```
|
|
122
191
|
- Ensure the **New Architecture** is enabled (`newArchEnabled=true` in
|
|
123
192
|
`android/gradle.properties`; `RCT_NEW_ARCH_ENABLED=1` for the iOS Podfile install),
|
|
@@ -235,6 +304,7 @@ export default function VerifyScreen() {
|
|
|
235
304
|
| `enableSelfie` | `boolean` | `true` | Capture a selfie during liveness. |
|
|
236
305
|
| `enableDocumentCapture` | `boolean` | `true` | Enable the document-scan step for document IDs. |
|
|
237
306
|
| `allowDocumentUpload` | `boolean` | `true` | Allow picking a document photo from the gallery as an alternative to the camera. `false` hides every "upload instead" affordance (it's still offered on the camera-permission-denied screen as an escape hatch). |
|
|
307
|
+
| `allowDocumentScan` | `boolean` | `true` | Allow photographing the document with the live camera. `false` makes document capture upload-only: the SDK never asks for camera access and the applicant chooses a photo of each side from their device. At least one of `allowDocumentScan` and `allowDocumentUpload` must stay on; a config that switches both off keeps the camera on. |
|
|
238
308
|
| `enableLiveness` | `boolean` | `true` | Run the liveness challenge step. The server can still disable it per ID type. |
|
|
239
309
|
| `livenessMode` | `'gestures' \| 'flash' \| 'both'` | `'gestures'` | How liveness is proven. See [Liveness](#liveness). |
|
|
240
310
|
| `flashSequenceLength` | `number` (2–5) | `4` | Colours in the flash sequence, for `'flash'` / `'both'`. |
|
|
@@ -527,7 +597,9 @@ terminal `4xx` surface immediately. The UI shows a top toast while retrying, and
|
|
|
527
597
|
If the user denies camera access, the SDK shows a clear "camera access needed"
|
|
528
598
|
screen (with an **Open Settings** action) instead of hanging, and reports
|
|
529
599
|
`camera_permission_denied` to `onError`. Document capture additionally offers a
|
|
530
|
-
gallery-upload fallback unless `allowDocumentUpload` is `false`.
|
|
600
|
+
gallery-upload fallback unless `allowDocumentUpload` is `false`. When
|
|
601
|
+
`allowDocumentScan` is `false` the document step never requests the camera, so
|
|
602
|
+
this screen cannot appear there: each side is a photo chosen from the device.
|
|
531
603
|
|
|
532
604
|
### Liveness quality guards
|
|
533
605
|
|
|
@@ -553,6 +625,67 @@ v5 Nitro frame processor: the camera frame never crosses the JS bridge. The self
|
|
|
553
625
|
is **auto-captured** once challenges pass (anti-spoofing — never user-triggered),
|
|
554
626
|
and a short liveness video is recorded and uploaded best-effort.
|
|
555
627
|
|
|
628
|
+
## App size
|
|
629
|
+
|
|
630
|
+
The SDK adds native machine learning to a host app, and that is where the weight
|
|
631
|
+
sits. Two things decide what a user actually downloads, and the defaults are
|
|
632
|
+
already the small ones — but the third and fourth are the host app's to set, and
|
|
633
|
+
they are worth more than everything the SDK can do on its own.
|
|
634
|
+
|
|
635
|
+
**On-device models are fetched, not bundled.** Face detection and text
|
|
636
|
+
recognition both run on Google ML Kit on Android, and the SDK depends on the
|
|
637
|
+
Play Services variants, which download their models on first use. Measured on a
|
|
638
|
+
real integrator's release APK, the bundled pair cost **18.5 MB per device**
|
|
639
|
+
(arm64: text 10.55 MB, face 7.95 MB) plus `.tflite` files in `assets/`, which
|
|
640
|
+
ship to every device because assets are not split by ABI. Fetched, that is about
|
|
641
|
+
0.4 MB of shims.
|
|
642
|
+
|
|
643
|
+
The SDK primes both downloads the moment the flow opens, so they overlap the
|
|
644
|
+
consent and ID-type screens. If a model has not arrived by the time it is needed,
|
|
645
|
+
the step says so rather than failing silently — the liveness step waits and
|
|
646
|
+
explains, and the MRZ scanner tells the user the code cannot be read and lets
|
|
647
|
+
them continue without the chip.
|
|
648
|
+
|
|
649
|
+
The trade is real: the Play Services variants need Google Play Services, so they
|
|
650
|
+
do not work on Huawei or bare AOSP builds. If you ship to those devices, put this
|
|
651
|
+
in your root `build.gradle` and you get fully-offline models back, at 18.5 MB per
|
|
652
|
+
device:
|
|
653
|
+
|
|
654
|
+
```gradle
|
|
655
|
+
ext { myazaKycBundledMlKit = true }
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
**Ship an App Bundle, or filter your ABIs.** Native libraries dominate the rest
|
|
659
|
+
of the download, and a universal APK carries every architecture at once. An `.aab`
|
|
660
|
+
lets Play deliver only the one a device needs. If you must ship an APK, name the
|
|
661
|
+
architectures your users actually have:
|
|
662
|
+
|
|
663
|
+
```gradle
|
|
664
|
+
android {
|
|
665
|
+
defaultConfig {
|
|
666
|
+
ndk { abiFilters 'arm64-v8a', 'armeabi-v7a' }
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
**Turn on R8 and resource shrinking.** The SDK ships its own consumer rules
|
|
672
|
+
(`consumer-rules.pro`), so you do not need to learn which of its classes Nitro
|
|
673
|
+
constructs by name:
|
|
674
|
+
|
|
675
|
+
```gradle
|
|
676
|
+
android {
|
|
677
|
+
buildTypes {
|
|
678
|
+
release {
|
|
679
|
+
minifyEnabled true
|
|
680
|
+
shrinkResources true
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
**Use Expo SDK 54 or newer.** Its default template builds smaller than earlier
|
|
687
|
+
ones, and the SDK's peer range assumes it.
|
|
688
|
+
|
|
556
689
|
## Documentation
|
|
557
690
|
|
|
558
691
|
Full documentation, configuration options, and webhook setup: **[trust.myaza.co/documentation/sdks](https://trust.myaza.co/documentation/sdks)**.
|
|
@@ -560,3 +693,140 @@ Full documentation, configuration options, and webhook setup: **[trust.myaza.co/
|
|
|
560
693
|
## License
|
|
561
694
|
|
|
562
695
|
MIT © Flitstack Technologies Inc.
|
|
696
|
+
|
|
697
|
+
## Presence reporting (Address Intelligence)
|
|
698
|
+
|
|
699
|
+
When a workflow enables presence verification (`addressCollection.presence.enabled`),
|
|
700
|
+
the SDK stores the confirmed pin on-device at capture. Call the reporter from your
|
|
701
|
+
app on a natural moment (app open works well):
|
|
702
|
+
|
|
703
|
+
```tsx
|
|
704
|
+
import { reportAddressPresence, clearPresencePin } from '@myazahq/kyc-sdk-react-native';
|
|
705
|
+
|
|
706
|
+
const result = await reportAddressPresence({
|
|
707
|
+
apiKey: 'pk_live_…',
|
|
708
|
+
externalUserId: 'user_42', // the same userId the KYC flow ran with
|
|
709
|
+
});
|
|
710
|
+
// result.reason: 'reported' | 'no_pin' | 'services_off' | 'no_fix' | 'outside_fence' | 'network_error'
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
It never throws and never blocks startup. The geofence is evaluated ON-DEVICE:
|
|
714
|
+
only the derived record (calendar day + a night flag) is transmitted, never a
|
|
715
|
+
coordinate. A fix outside the fence sends nothing (the server scores presence,
|
|
716
|
+
never absence); a mock-location fix is reported flagged. `clearPresencePin`
|
|
717
|
+
drops the stored pin (sign-out, or once the watch resolves).
|
|
718
|
+
|
|
719
|
+
### Background monitoring (OS geofencing)
|
|
720
|
+
|
|
721
|
+
The stronger tier: the OS wakes the SDK on fence crossings around the stored
|
|
722
|
+
pin, app closed or not, so dwell and nights accrue with nobody in the loop.
|
|
723
|
+
Entries stamp a timestamp; exits fold the dwell span into per-day aggregates
|
|
724
|
+
and flush them. As with the foreground tier, only the derived day records
|
|
725
|
+
ever leave the phone.
|
|
726
|
+
|
|
727
|
+
Three opt-ins, each deliberate:
|
|
728
|
+
|
|
729
|
+
1. Install the optional peer: `npx expo install expo-task-manager` (without
|
|
730
|
+
it the background tier simply does not exist — never a crash).
|
|
731
|
+
2. Declare background location via the config plugin — this is what changes
|
|
732
|
+
your app's store review posture, so it is never a default:
|
|
733
|
+
|
|
734
|
+
```json
|
|
735
|
+
["@myazahq/kyc-sdk-react-native", { "location": "always" }]
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
3. Register the task at your app's ROOT module (before the component tree —
|
|
739
|
+
a task defined inside a component never fires headlessly), then enable:
|
|
740
|
+
|
|
741
|
+
```tsx
|
|
742
|
+
// index.js
|
|
743
|
+
import { registerBackgroundPresence } from '@myazahq/kyc-sdk-react-native';
|
|
744
|
+
registerBackgroundPresence();
|
|
745
|
+
|
|
746
|
+
// later, after the KYC flow stored a pin:
|
|
747
|
+
const result = await enableBackgroundPresence({
|
|
748
|
+
apiKey: 'pk_live_…',
|
|
749
|
+
externalUserId: 'user_42',
|
|
750
|
+
});
|
|
751
|
+
// result.reason: 'enabled' | 'module_missing' | 'no_pin'
|
|
752
|
+
// | 'foreground_denied' | 'background_denied' | 'start_failed'
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
`disableBackgroundPresence()` disarms the fence. A permission refusal leaves
|
|
756
|
+
the foreground tier working exactly as before — the tiers degrade, never
|
|
757
|
+
break.
|
|
758
|
+
|
|
759
|
+
### The Android foreground service (reliability on OEM-managed phones)
|
|
760
|
+
|
|
761
|
+
A geofence alone is not reliable on Android once a manufacturer's battery
|
|
762
|
+
manager decides your app is idle: transitions are dropped, nothing says so,
|
|
763
|
+
and the watch quietly lapses to inconclusive. The phones on that list (Tecno,
|
|
764
|
+
Infinix, itel, Xiaomi, Oppo, Vivo) are the ones the market carries. A
|
|
765
|
+
foreground service, with its persistent notification, is the one thing those
|
|
766
|
+
managers leave alone — and OkHi's own integration guidance for the same
|
|
767
|
+
markets is exactly this.
|
|
768
|
+
|
|
769
|
+
Opt-in, Android only (iOS region monitoring is reliable on its own), on the
|
|
770
|
+
same `location: "always"` plugin setting, which also declares the
|
|
771
|
+
`FOREGROUND_SERVICE` and `FOREGROUND_SERVICE_LOCATION` permissions it needs.
|
|
772
|
+
`registerBackgroundPresence()` at the root already defines its task; then:
|
|
773
|
+
|
|
774
|
+
```tsx
|
|
775
|
+
const result = await enableForegroundService({
|
|
776
|
+
apiKey: 'pk_live_…',
|
|
777
|
+
externalUserId: 'user_42',
|
|
778
|
+
notification: {
|
|
779
|
+
title: 'Address verification in progress', // shown in the status bar
|
|
780
|
+
body: 'Open the app to see your progress',
|
|
781
|
+
color: '#5645F5',
|
|
782
|
+
},
|
|
783
|
+
});
|
|
784
|
+
// result.reason: 'enabled' | 'unsupported_platform' | 'module_missing' | 'no_pin'
|
|
785
|
+
// | 'foreground_denied' | 'background_denied' | 'start_failed'
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
While it runs, a low-power fix every ten minutes (or hundred metres) is
|
|
789
|
+
turned into the same enter/exit spans the geofence folds, on the same stored
|
|
790
|
+
state, so the two never double-count a stay; the queue flushes while the
|
|
791
|
+
process is alive; and a fence the OS dropped (a location toggle clears every
|
|
792
|
+
registered fence) is re-armed. `disableForegroundService()` stops it and its
|
|
793
|
+
notification. Word the notification honestly — it is on screen for days.
|
|
794
|
+
|
|
795
|
+
### Which tier is running?
|
|
796
|
+
|
|
797
|
+
Permissions get revoked in Settings and nothing tells the app. Ask:
|
|
798
|
+
|
|
799
|
+
```tsx
|
|
800
|
+
import { presenceStatus, openLocationSettings } from '@myazahq/kyc-sdk-react-native';
|
|
801
|
+
|
|
802
|
+
const status = await presenceStatus('user_42');
|
|
803
|
+
// status.tier: 'background' | 'foreground' | 'none'
|
|
804
|
+
// plus pinStored, alwaysOn, locationServicesEnabled, both permission states,
|
|
805
|
+
// geofenceArmed, foregroundServiceRunning
|
|
806
|
+
if (!status.locationServicesEnabled) {
|
|
807
|
+
// The phone's location toggle is off: permission granted or not, no fix
|
|
808
|
+
// can be taken. Android deep-links to the toggle itself.
|
|
809
|
+
await openLocationSettings('services');
|
|
810
|
+
} else if (status.tier === 'none' && status.pinStored) {
|
|
811
|
+
// The road back runs through Settings — no OS allows re-prompting in-app.
|
|
812
|
+
await openLocationSettings();
|
|
813
|
+
}
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
### Showing the person where the check stands
|
|
817
|
+
|
|
818
|
+
Somebody kept from a feature until their address is verified should be able
|
|
819
|
+
to see the progress in your app, without a webhook relayed through your
|
|
820
|
+
backend. The status endpoint is publishable-safe and enumeration-safe (an
|
|
821
|
+
unknown user and a user with no watch answer the same `not_started` shape):
|
|
822
|
+
|
|
823
|
+
```ts
|
|
824
|
+
const res = await fetch(`${serverUrl}/api/kyc/address/presence/${externalUserId}`, {
|
|
825
|
+
headers: { Authorization: `Bearer pk_live_…` },
|
|
826
|
+
});
|
|
827
|
+
const { status, progress, tier } = await res.json();
|
|
828
|
+
// status: 'not_started' | 'in_progress' | 'verified' | 'failed' | 'inconclusive' | 'expired' | 'revoked'
|
|
829
|
+
// progress.score: 0..1 on WEIGHTED evidence (five foreground nights and three
|
|
830
|
+
// geofence nights both read 1); nightsObserved/daysObserved beside it
|
|
831
|
+
// tier: 'background' | 'foreground' | null — what is actually feeding it
|
|
832
|
+
```
|
package/android/build.gradle
CHANGED
|
@@ -32,6 +32,11 @@ def safeExtGet(prop, fallback) {
|
|
|
32
32
|
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// Resolved ONCE and used twice: it picks the ML Kit artifacts below AND is
|
|
36
|
+
// compiled into BuildConfig so the native readiness checks can answer without
|
|
37
|
+
// asking Play Services about a model that is sitting in the APK.
|
|
38
|
+
def bundledMlKit = safeExtGet('myazaKycBundledMlKit', false).toString().toBoolean()
|
|
39
|
+
|
|
35
40
|
android {
|
|
36
41
|
namespace 'co.myazahq.kyc.rn'
|
|
37
42
|
compileSdk safeExtGet('compileSdkVersion', 34)
|
|
@@ -41,6 +46,17 @@ android {
|
|
|
41
46
|
minSdk safeExtGet('minSdkVersion', 24)
|
|
42
47
|
targetSdk safeExtGet('targetSdkVersion', 34)
|
|
43
48
|
|
|
49
|
+
// Shipped to the host app's shrinker so an integrator can enable R8 without
|
|
50
|
+
// learning which of our classes Nitro constructs by name. See the file.
|
|
51
|
+
consumerProguardFiles 'consumer-rules.pro'
|
|
52
|
+
|
|
53
|
+
// Whether the ML Kit models ship inside the APK. Read by the face detector
|
|
54
|
+
// and the text recogniser to short-circuit their readiness checks: on a
|
|
55
|
+
// bundled build the model is already here, and asking Play Services whether
|
|
56
|
+
// it holds the OPTIONAL module would answer "no" and strand a build that
|
|
57
|
+
// works perfectly offline.
|
|
58
|
+
buildConfigField "boolean", "BUNDLED_ML_KIT", "${bundledMlKit}"
|
|
59
|
+
|
|
44
60
|
externalNativeBuild {
|
|
45
61
|
cmake {
|
|
46
62
|
cppFlags "-frtti -fexceptions -Wall -Wextra -fstack-protector-all"
|
|
@@ -125,24 +141,22 @@ dependencies {
|
|
|
125
141
|
// GMS. An org shipping to those devices sets `myazaKycBundledMlKit = true` in
|
|
126
142
|
// its root build.gradle ext and gets today's fully-offline behaviour back.
|
|
127
143
|
//
|
|
128
|
-
// Availability is NOT inferred per frame
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
144
|
+
// Availability is NOT inferred per frame. Both models carry the same
|
|
145
|
+
// `isModelReady()` / `prepareModel()` contract, checked once before the
|
|
146
|
+
// camera opens, because the failure they guard against is identical: an
|
|
147
|
+
// absent model is indistinguishable from an empty frame (no face found, no
|
|
148
|
+
// text found), so a fresh install would sit on "position your face" or never
|
|
149
|
+
// auto-capture, with nothing able to say why.
|
|
150
|
+
//
|
|
151
|
+
// The Kotlin and Swift sources are unchanged by the switch — the unbundled
|
|
152
|
+
// artifacts expose the same `com.google.mlkit.vision.*` API, and the
|
|
153
|
+
// OptionalModuleApi the readiness check needs comes from the shared
|
|
154
|
+
// play-services common artifact both variants depend on.
|
|
155
|
+
if (bundledMlKit) {
|
|
132
156
|
implementation 'com.google.mlkit:face-detection:16.1.6'
|
|
157
|
+
implementation 'com.google.mlkit:text-recognition:16.0.1'
|
|
133
158
|
} else {
|
|
134
159
|
implementation 'com.google.android.gms:play-services-mlkit-face-detection:17.1.0'
|
|
160
|
+
implementation 'com.google.android.gms:play-services-mlkit-text-recognition:19.0.1'
|
|
135
161
|
}
|
|
136
|
-
|
|
137
|
-
// Text recognition is STILL BUNDLED, deliberately, and it is the larger of the
|
|
138
|
-
// two (10.55 MB vs 7.95 MB on arm64) — so this is the bigger half of the win,
|
|
139
|
-
// deferred rather than abandoned.
|
|
140
|
-
//
|
|
141
|
-
// Unbundling it needs the same isModelReady()/prepareModel() contract the face
|
|
142
|
-
// detector just gained. Without that, a missing OCR model is indistinguishable
|
|
143
|
-
// from "no text in frame": document auto-capture would simply never fire and
|
|
144
|
-
// MRZ scanning would never produce a key for the chip read, with nothing shown
|
|
145
|
-
// to the user. Shipping the dependency swap ahead of the availability handling
|
|
146
|
-
// would trade 10 MB for a silent failure on every fresh install.
|
|
147
|
-
implementation 'com.google.mlkit:text-recognition:16.0.1'
|
|
148
162
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Consumer R8 / ProGuard rules for @myazahq/kyc-sdk-react-native.
|
|
2
|
+
#
|
|
3
|
+
# Merged into the host app's shrinker configuration automatically through
|
|
4
|
+
# `consumerProguardFiles`, so an integrator who enables R8 (`minifyEnabled true`,
|
|
5
|
+
# `shrinkResources true`) needs nothing on their side. Enabling R8 is the single
|
|
6
|
+
# biggest thing an integrator can do for the Java half of the SDK's footprint:
|
|
7
|
+
# the unminified dex the SDK pulls in (CameraX, Play Services shims, Expo, Kotlin
|
|
8
|
+
# reflection) is mostly code no flow ever calls.
|
|
9
|
+
#
|
|
10
|
+
# Nitro constructs every hybrid implementation BY CLASS NAME from C++: the
|
|
11
|
+
# generated OnLoad registers one JNI constructor per spec, which the shrinker
|
|
12
|
+
# cannot see. A shrunk build without these rules removes exactly the classes the
|
|
13
|
+
# registry then fails to find, and the failure is a crash at the first camera
|
|
14
|
+
# frame rather than a build error.
|
|
15
|
+
-keep class com.margelo.nitro.** { *; }
|
|
16
|
+
-keep class co.myazahq.kyc.rn.** { *; }
|
|
17
|
+
|
|
18
|
+
# JJ2000, the JPEG 2000 decoder for the chip portrait, predates the shrinker
|
|
19
|
+
# era. Keeping it whole costs about 0.3 MB and takes a whole class of
|
|
20
|
+
# shrink-time surprises out of a decode that runs once per chip read.
|
|
21
|
+
-keep class jj2000.** { *; }
|
|
22
|
+
-dontwarn jj2000.**
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
2
|
<!--
|
|
3
|
-
NFC
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
NFC is DELIBERATELY NOT DECLARED HERE.
|
|
4
|
+
======================================
|
|
5
|
+
Reading a passport's eMRTD chip needs android.permission.NFC, but a library
|
|
6
|
+
manifest is MERGED into every host app, so declaring it here gave the
|
|
7
|
+
permission to every integrator — including the majority whose workflows
|
|
8
|
+
never touch a chip, and who had no way to remove it. That is the opposite
|
|
9
|
+
of opt-in, and it silently contradicted the config plugin's own `nfc`
|
|
10
|
+
option, which defaults to OFF.
|
|
11
|
+
|
|
12
|
+
The permission AND the feature are now written by app.plugin.js, and only
|
|
13
|
+
on `nfc: true`. They must travel TOGETHER — see ensureNfcFeatureOptional()
|
|
14
|
+
there for why the feature is not an optional extra: Android infers
|
|
15
|
+
`required="true"` from the permission alone, which hides the host app from
|
|
16
|
+
every device with no NFC radio.
|
|
17
|
+
|
|
18
|
+
The chip step still checks availability at runtime and auto-skips on a
|
|
19
|
+
device with no radio, so a host that opts out loses nothing else.
|
|
7
20
|
-->
|
|
8
|
-
<uses-permission android:name="android.permission.NFC" />
|
|
9
|
-
<uses-feature android:name="android.hardware.nfc" android:required="false" />
|
|
10
21
|
|
|
11
22
|
<application>
|
|
12
23
|
<!--
|
|
@@ -9,9 +9,6 @@ import com.google.mlkit.vision.face.Face
|
|
|
9
9
|
import com.google.mlkit.vision.face.FaceDetection
|
|
10
10
|
import com.google.mlkit.vision.face.FaceDetector
|
|
11
11
|
import com.google.mlkit.vision.face.FaceDetectorOptions
|
|
12
|
-
import com.google.android.gms.common.moduleinstall.ModuleInstall
|
|
13
|
-
import com.google.android.gms.common.moduleinstall.ModuleInstallRequest
|
|
14
|
-
import com.margelo.nitro.NitroModules
|
|
15
12
|
import com.margelo.nitro.core.Promise
|
|
16
13
|
import java.util.concurrent.CountDownLatch
|
|
17
14
|
import java.util.concurrent.TimeUnit
|
|
@@ -45,64 +42,13 @@ class HybridMyazaFaceDetector : HybridMyazaFaceDetectorSpec() {
|
|
|
45
42
|
|
|
46
43
|
// ── Model availability ───────────────────────────────────────────────────
|
|
47
44
|
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
// first launch before the download completes, or a device with no GMS at all.
|
|
52
|
-
//
|
|
53
|
-
// `detectFace` CANNOT report that. Its only channel is FaceResult, where a
|
|
54
|
-
// missing model and an empty frame are both `faceCount: 0` — so a user on a
|
|
55
|
-
// fresh install would watch "position your face" forever with nothing to
|
|
56
|
-
// explain it. Hence a separate, explicit contract, checked before the camera
|
|
57
|
-
// opens rather than inferred per frame.
|
|
58
|
-
//
|
|
59
|
-
// Cached rather than queried live: `areModulesAvailable` is asynchronous and
|
|
60
|
-
// `isModelReady` is a synchronous Nitro call, so blocking the worklet thread
|
|
61
|
-
// to answer it would be worse than the problem. The flag starts false and is
|
|
62
|
-
// set by prepareModel(), which the SDK primes at flow start.
|
|
63
|
-
@Volatile private var modelReady = false
|
|
45
|
+
// Shared with the text recogniser: the rule and the reasoning are identical,
|
|
46
|
+
// so they live once in MlKitModelReadiness.kt rather than twice here.
|
|
47
|
+
private val readiness = MlKitModelReadiness(detector)
|
|
64
48
|
|
|
65
|
-
override fun isModelReady(): Boolean =
|
|
49
|
+
override fun isModelReady(): Boolean = readiness.isReady()
|
|
66
50
|
|
|
67
|
-
override fun prepareModel(): Promise<Boolean> = Promise.async {
|
|
68
|
-
if (modelReady) return@async true
|
|
69
|
-
|
|
70
|
-
val ctx = NitroModules.applicationContext
|
|
71
|
-
?: return@async false // No context — cannot ask Play Services anything.
|
|
72
|
-
|
|
73
|
-
val latch = CountDownLatch(1)
|
|
74
|
-
var ok = false
|
|
75
|
-
try {
|
|
76
|
-
val client = ModuleInstall.getClient(ctx)
|
|
77
|
-
// areModulesAvailable() answers "is it already here"; deferredInstall()
|
|
78
|
-
// asks Play Services to fetch it in the background if not. Requesting the
|
|
79
|
-
// install unconditionally is simpler AND correct — it is a no-op when the
|
|
80
|
-
// module is already present, and starting it early is the entire point.
|
|
81
|
-
client.areModulesAvailable(detector)
|
|
82
|
-
.addOnSuccessListener { response ->
|
|
83
|
-
ok = response.areModulesAvailable()
|
|
84
|
-
if (!ok) {
|
|
85
|
-
client.deferredInstall(
|
|
86
|
-
ModuleInstallRequest.newBuilder().addApi(detector).build(),
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
latch.countDown()
|
|
90
|
-
}
|
|
91
|
-
.addOnFailureListener {
|
|
92
|
-
// Thrown on devices without Google Play Services at all (Huawei, bare
|
|
93
|
-
// AOSP). Not an error to retry — it will never succeed on this device.
|
|
94
|
-
// Such orgs should build with `myazaKycBundledMlKit = true`.
|
|
95
|
-
ok = false
|
|
96
|
-
latch.countDown()
|
|
97
|
-
}
|
|
98
|
-
latch.await(MODEL_CHECK_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
|
99
|
-
} catch (_: Throwable) {
|
|
100
|
-
ok = false
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
modelReady = ok
|
|
104
|
-
ok
|
|
105
|
-
}
|
|
51
|
+
override fun prepareModel(): Promise<Boolean> = Promise.async { readiness.prepare() }
|
|
106
52
|
|
|
107
53
|
@ExperimentalGetImage
|
|
108
54
|
override fun detectFace(frame: HybridFrameSpec): FaceResult {
|
|
@@ -119,13 +65,34 @@ class HybridMyazaFaceDetector : HybridMyazaFaceDetectorSpec() {
|
|
|
119
65
|
val rotation = proxy.imageInfo.rotationDegrees
|
|
120
66
|
val input = InputImage.fromMediaImage(mediaImage, rotation)
|
|
121
67
|
|
|
68
|
+
// Read the frame's dimensions HERE, synchronously, and let the callback use
|
|
69
|
+
// these ints rather than the Image.
|
|
70
|
+
//
|
|
71
|
+
// The listener runs on the main Looper whenever ML Kit finishes, which is
|
|
72
|
+
// not necessarily before `latch.await` below gives up. Once detectFace has
|
|
73
|
+
// returned, VisionCamera recycles the frame and closes the underlying
|
|
74
|
+
// android.media.Image — so a late callback touching `mediaImage.width`
|
|
75
|
+
// threw `IllegalStateException: Image is already closed` and took the whole
|
|
76
|
+
// app down, every time a face was actually FOUND (the failure listener
|
|
77
|
+
// reads nothing, so only success crashed).
|
|
78
|
+
//
|
|
79
|
+
// Unbundling the models is what exposed it: play-services-mlkit dispatches
|
|
80
|
+
// through Play Services IPC and is slower than the in-process bundled
|
|
81
|
+
// artifact, so the callback overruns the 200 ms wait routinely on real
|
|
82
|
+
// mid-range hardware. Seen on a TECNO KM5 (Android 15).
|
|
83
|
+
//
|
|
84
|
+
// The text recogniser already reads its dimensions up front for the same
|
|
85
|
+
// reason; this makes the two agree.
|
|
86
|
+
val imageWidth = mediaImage.width
|
|
87
|
+
val imageHeight = mediaImage.height
|
|
88
|
+
|
|
122
89
|
var result: FaceResult? = null
|
|
123
90
|
val latch = CountDownLatch(1)
|
|
124
91
|
detector.process(input)
|
|
125
92
|
.addOnSuccessListener { faces ->
|
|
126
93
|
val face = faces.maxByOrNull { it.boundingBox.width() * it.boundingBox.height() }
|
|
127
94
|
result = face?.let {
|
|
128
|
-
toResult(it,
|
|
95
|
+
toResult(it, imageWidth, imageHeight, rotation, faces.size, brightness, faceRgb)
|
|
129
96
|
}
|
|
130
97
|
latch.countDown()
|
|
131
98
|
}
|
|
@@ -283,14 +250,4 @@ class HybridMyazaFaceDetector : HybridMyazaFaceDetectorSpec() {
|
|
|
283
250
|
faceG = -1.0,
|
|
284
251
|
faceB = -1.0,
|
|
285
252
|
)
|
|
286
|
-
|
|
287
|
-
private companion object {
|
|
288
|
-
/**
|
|
289
|
-
* Bound on the availability query. It is a local Play Services call, not the
|
|
290
|
-
* model download — the download runs in the background afterwards. Generous
|
|
291
|
-
* enough for a cold Play Services process, short enough that a wedged one
|
|
292
|
-
* cannot stall flow start.
|
|
293
|
-
*/
|
|
294
|
-
const val MODEL_CHECK_TIMEOUT_MS = 3_000L
|
|
295
|
-
}
|
|
296
253
|
}
|
|
@@ -32,6 +32,20 @@ class HybridMyazaTextRecognizer : HybridMyazaTextRecognizerSpec() {
|
|
|
32
32
|
private val recognizer: TextRecognizer =
|
|
33
33
|
TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
|
|
34
34
|
|
|
35
|
+
// ── Model availability ───────────────────────────────────────────────────
|
|
36
|
+
//
|
|
37
|
+
// Shared with the face detector, because the failure is the same one: the
|
|
38
|
+
// model is fetched through Play Services rather than bundled, and a missing
|
|
39
|
+
// one is indistinguishable from a frame with no text in it. Left to the
|
|
40
|
+
// per-frame result, document auto-capture would simply never fire and the MRZ
|
|
41
|
+
// would never produce a key for the chip read, with nothing shown to explain
|
|
42
|
+
// it. See MlKitModelReadiness.kt.
|
|
43
|
+
private val readiness = MlKitModelReadiness(recognizer)
|
|
44
|
+
|
|
45
|
+
override fun isModelReady(): Boolean = readiness.isReady()
|
|
46
|
+
|
|
47
|
+
override fun prepareModel(): Promise<Boolean> = Promise.async { readiness.prepare() }
|
|
48
|
+
|
|
35
49
|
@ExperimentalGetImage
|
|
36
50
|
override fun recognizeText(frame: HybridFrameSpec, bottomFraction: Double): TextResult {
|
|
37
51
|
val proxy: ImageProxy = (frame as? NativeFrame)?.image ?: return TextResult(emptyArray())
|