@myazahq/kyc-sdk-react-native 2.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 (87) hide show
  1. package/KycSdkReactNative.podspec +32 -0
  2. package/LICENSE +21 -0
  3. package/README.md +164 -0
  4. package/android/CMakeLists.txt +29 -0
  5. package/android/build.gradle +110 -0
  6. package/android/src/main/AndroidManifest.xml +12 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +12 -0
  8. package/android/src/main/java/co/myazahq/kyc/rn/MyazaFaceDetectorPackage.kt +36 -0
  9. package/android/src/main/java/co/myazahq/kyc/rn/MyazaStatusBarModule.kt +64 -0
  10. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +134 -0
  11. package/app.plugin.js +55 -0
  12. package/expo-module.config.json +6 -0
  13. package/ios/HybridMyazaFaceDetector.swift +233 -0
  14. package/package.json +84 -0
  15. package/react-native.config.js +23 -0
  16. package/src/MyazaKYC.tsx +235 -0
  17. package/src/__tests__/cardCrop.test.ts +39 -0
  18. package/src/__tests__/deviceMetadata.test.ts +34 -0
  19. package/src/__tests__/errors.test.ts +34 -0
  20. package/src/__tests__/flow.test.ts +61 -0
  21. package/src/__tests__/gestureDetector.test.ts +37 -0
  22. package/src/__tests__/liveness.test.ts +112 -0
  23. package/src/__tests__/resolveUrl.test.ts +64 -0
  24. package/src/__tests__/validators.test.ts +38 -0
  25. package/src/assets/liveness/Blink.gif +0 -0
  26. package/src/assets/liveness/Nod.gif +0 -0
  27. package/src/assets/liveness/Smile.gif +0 -0
  28. package/src/assets/liveness/Turn.gif +0 -0
  29. package/src/components/CameraPermissionView.tsx +116 -0
  30. package/src/components/CameraViewfinder.tsx +156 -0
  31. package/src/components/CountryFlag.tsx +52 -0
  32. package/src/components/DocumentCropper.tsx +325 -0
  33. package/src/components/GlassIconButton.tsx +92 -0
  34. package/src/components/Icon.tsx +125 -0
  35. package/src/components/KycFlow.tsx +205 -0
  36. package/src/components/KycSheet.tsx +224 -0
  37. package/src/components/MyazaAlert.tsx +57 -0
  38. package/src/components/MyazaButton.tsx +101 -0
  39. package/src/components/MyazaCard.tsx +48 -0
  40. package/src/components/MyazaInput.tsx +112 -0
  41. package/src/components/MyazaPulseLoader.tsx +71 -0
  42. package/src/components/StatusBarController.tsx +42 -0
  43. package/src/components/StepHeader.tsx +56 -0
  44. package/src/components/StepIndicator.tsx +74 -0
  45. package/src/components/Typography.tsx +69 -0
  46. package/src/components/fonts.ts +49 -0
  47. package/src/components/glass/GlassGroup.tsx +34 -0
  48. package/src/components/glass/GlassSurface.tsx +64 -0
  49. package/src/components/runtime.tsx +93 -0
  50. package/src/components/toast.tsx +154 -0
  51. package/src/components/useBranding.ts +27 -0
  52. package/src/components/useVideoRecorder.ts +122 -0
  53. package/src/config/captureSettings.ts +67 -0
  54. package/src/config/idTypes.ts +79 -0
  55. package/src/config/theme.ts +186 -0
  56. package/src/index.ts +54 -0
  57. package/src/liveness/challengeManager.ts +130 -0
  58. package/src/liveness/faceDetector.ts +79 -0
  59. package/src/liveness/gestureDetector.ts +80 -0
  60. package/src/liveness/speech.ts +66 -0
  61. package/src/liveness/types.ts +99 -0
  62. package/src/liveness/useLiveness.ts +484 -0
  63. package/src/liveness/visionCameraFaceDetector.ts +118 -0
  64. package/src/screens/ConsentStep.tsx +164 -0
  65. package/src/screens/DocumentCaptureStep.tsx +500 -0
  66. package/src/screens/IdInputStep.tsx +79 -0
  67. package/src/screens/IdTypeStep.tsx +142 -0
  68. package/src/screens/LivenessAvatar.tsx +69 -0
  69. package/src/screens/LivenessStep.tsx +615 -0
  70. package/src/screens/SubmittedStep.tsx +177 -0
  71. package/src/services/api.ts +291 -0
  72. package/src/services/cardCrop.ts +52 -0
  73. package/src/services/deviceMetadata.ts +185 -0
  74. package/src/services/errors.ts +92 -0
  75. package/src/services/mediaCompress.ts +129 -0
  76. package/src/services/resolveUrl.ts +98 -0
  77. package/src/services/retry.ts +70 -0
  78. package/src/services/validators.ts +103 -0
  79. package/src/specs/MyazaFaceDetector.nitro.ts +44 -0
  80. package/src/store/kycStore.ts +288 -0
  81. package/src/store/serverConfig.ts +78 -0
  82. package/src/types/config.ts +239 -0
  83. package/src/types/country-flag-icons.d.ts +6 -0
  84. package/src/types/verification.ts +79 -0
  85. package/src/utils/platform.ts +23 -0
  86. package/src/utils/tokens.ts +10 -0
  87. package/src/utils/uuid.ts +31 -0
@@ -0,0 +1,32 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'KycSdkReactNative'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.license = package['license']
10
+ s.author = 'Myaza'
11
+ s.homepage = 'https://github.com/myazahq/kyc-sdk-react-native'
12
+ s.platforms = { :ios => '15.1' }
13
+ s.swift_version = '5.9'
14
+ s.source = { :git => 'https://github.com/myazahq/kyc-sdk-react-native.git', :tag => "#{s.version}" }
15
+
16
+ # Apple Vision face-detector — a VisionCamera v5 Nitro HybridObject
17
+ # (ios/HybridMyazaFaceDetector.swift), generated from
18
+ # src/specs/MyazaFaceDetector.nitro.ts. No GoogleMLKit pod — iOS uses Vision,
19
+ # so the SDK still builds on Apple-Silicon iOS simulators.
20
+ s.source_files = 'ios/**/*.{h,m,mm,swift}'
21
+
22
+ s.dependency 'React-Core'
23
+ s.dependency 'VisionCamera'
24
+
25
+ # Add all files generated by Nitrogen (the C++/Swift specs, the Swift↔C++
26
+ # bridge, and the C++20 / SWIFT_OBJC_INTEROP_MODE=objcxx build settings the
27
+ # Nitro HybridObject needs) and the NitroModules dependency. Registration is
28
+ # handled by Nitro autolinking — no ObjC `+load` macro, so the old `-ObjC`
29
+ # dead-strip workaround is gone.
30
+ load 'nitrogen/generated/ios/KycSdkReactNative+autolinking.rb'
31
+ add_nitrogen_files(s)
32
+ end
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Flitstack Technologies Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # @myazahq/kyc-sdk-react-native
2
+
3
+ Myaza KYC SDK for **React Native (Expo)** — ID verification, document capture, and
4
+ active liveness detection. Mirrors the [web](https://www.npmjs.com/package/@myazahq/kyc-sdk-react)
5
+ and Flutter SDKs feature-for-feature and calls the same Myaza KYC API server.
6
+
7
+ The SDK is a **thin UI layer**: it captures the user's data (ID number, document
8
+ photos, a live selfie), uploads the media, and submits a verification request.
9
+ All verification (OCR, facial comparison, gov-DB checks) happens server-side and
10
+ is delivered asynchronously via webhook — the SDK is fire-and-forget.
11
+
12
+ ## Install
13
+
14
+ This library ships **native code** (an Apple Vision + Google ML Kit face-detector
15
+ VisionCamera frame-processor plugin), so it needs a custom native build. It does
16
+ **not** run in Expo Go. Pick your setup below.
17
+
18
+ Peer dependencies (both setups): `react-native-vision-camera`,
19
+ `react-native-worklets-core`, `react-native-svg`, and `expo` (the SDK uses several
20
+ `expo-*` modules — see *Bare React Native* if you're not on a managed Expo app).
21
+
22
+ ### Expo app (managed / prebuild — recommended)
23
+
24
+ ```sh
25
+ npx expo install @myazahq/kyc-sdk-react-native \
26
+ react-native-vision-camera react-native-worklets-core react-native-svg
27
+ ```
28
+
29
+ Add the config plugins to `app.json` (the SDK's plugin adds the iOS camera usage
30
+ string + Android `CAMERA`/`INTERNET` permissions; the VisionCamera plugin wires up
31
+ the camera + frame processors):
32
+
33
+ ```jsonc
34
+ // app.json
35
+ {
36
+ "expo": {
37
+ "plugins": [
38
+ ["react-native-vision-camera", { "enableMicrophonePermission": false }],
39
+ "@myazahq/kyc-sdk-react-native"
40
+ ]
41
+ }
42
+ }
43
+ ```
44
+
45
+ Then build a dev client (regenerates the native projects):
46
+
47
+ ```sh
48
+ npx expo prebuild
49
+ npx expo run:ios # or: npx expo run:android
50
+ ```
51
+
52
+ > The SDK plugin accepts an optional custom camera prompt:
53
+ > `["@myazahq/kyc-sdk-react-native", { "cameraPermission": "Your message…" }]`.
54
+
55
+ ### Bare React Native app (no Expo prebuild)
56
+
57
+ The SDK depends on a handful of `expo-*` modules, so install **`expo`** (the
58
+ package — you don't need the managed workflow) and let it autolink:
59
+
60
+ ```sh
61
+ # 1. Add the Expo module runtime to your bare app (one-time):
62
+ npx install-expo-modules@latest
63
+
64
+ # 2. Install the SDK + peers:
65
+ npm install @myazahq/kyc-sdk-react-native \
66
+ react-native-vision-camera react-native-worklets-core react-native-svg \
67
+ expo expo-image-manipulator expo-image-picker expo-speech expo-font \
68
+ expo-glass-effect expo-application expo-crypto expo-device expo-localization
69
+
70
+ # 3. iOS pods:
71
+ cd ios && pod install && cd ..
72
+ ```
73
+
74
+ Then add the native permissions yourself (the Expo config plugin only runs under
75
+ prebuild):
76
+
77
+ - **iOS** — add to `ios/<App>/Info.plist`:
78
+ ```xml
79
+ <key>NSCameraUsageDescription</key>
80
+ <string>We use the camera to photograph your ID and capture a live selfie.</string>
81
+ ```
82
+ - **Android** — add to `android/app/src/main/AndroidManifest.xml`:
83
+ ```xml
84
+ <uses-permission android:name="android.permission.CAMERA" />
85
+ <uses-permission android:name="android.permission.INTERNET" />
86
+ ```
87
+ - Enable VisionCamera frame processors (it needs `react-native-worklets-core`,
88
+ installed above) — see the
89
+ [VisionCamera setup docs](https://react-native-vision-camera.com/docs/guides).
90
+
91
+ The face-detector native module (podspec + Android Gradle project + the
92
+ `detectFace` frame-processor plugin) is autolinked via React Native / Expo
93
+ autolinking — no manual linking required.
94
+
95
+ > Voice guidance is **text-to-speech output only** — the SDK never records audio,
96
+ > so **no microphone permission** is requested or required
97
+ > (`enableMicrophonePermission: false`).
98
+
99
+ ## Usage
100
+
101
+ ```tsx
102
+ import { MyazaKYC } from '@myazahq/kyc-sdk-react-native';
103
+
104
+ export default function Screen() {
105
+ return (
106
+ <MyazaKYC
107
+ apiKey="pk_test_xxx" // prefix selects the env: pk_test_ → sandbox
108
+ country="NG"
109
+ idTypes={['bvn', 'nin', 'passport']}
110
+ enableDocumentCapture
111
+ enableLiveness
112
+ appearance={{ primaryColor: '#5645F5', companyName: 'Myaza', theme: 'light' }}
113
+ onSubmit={(s) => console.log('submitted', s.verificationId)}
114
+ onError={(e) => console.warn(e.code, e.message)}
115
+ />
116
+ );
117
+ }
118
+ ```
119
+
120
+ Or drive the flow from your own trigger with the hook:
121
+
122
+ ```tsx
123
+ import { useMyazaKYC } from '@myazahq/kyc-sdk-react-native';
124
+
125
+ const { open, close, isOpen, currentStep } = useMyazaKYC({ apiKey, country: 'NG' });
126
+ ```
127
+
128
+ ## Environment
129
+
130
+ The base URL is auto-detected from the API key prefix — there is **no**
131
+ `environment` prop:
132
+
133
+ | Prefix | Environment | Base URL |
134
+ | ---------- | ----------- | --------------------------------- |
135
+ | `pk_dev_` | development | `http://localhost:3001` (override with `devUrl`) |
136
+ | `pk_test_` | sandbox | `https://sandbox.identity.myaza.app` |
137
+ | `pk_live_` | production | `https://identity.myaza.app` |
138
+
139
+ ## Liveness
140
+
141
+ Active, challenge-based liveness (nod / turn / blink / smile — 2 randomly chosen
142
+ per session). Face detection runs **on-device, natively**:
143
+
144
+ - **iOS** — Apple Vision (`VNDetectFaceLandmarksRequest`).
145
+ - **Android** — Google ML Kit (native Gradle dep, Android-only — so no
146
+ cross-platform ML Kit iOS pod, and the SDK still builds on Apple-Silicon iOS
147
+ simulators).
148
+
149
+ Both run as a [react-native-vision-camera](https://github.com/mrousavy/react-native-vision-camera)
150
+ frame-processor plugin: the camera frame never crosses the JS bridge. The selfie
151
+ is **auto-captured** once challenges pass (anti-spoofing), and a short liveness
152
+ video is recorded and uploaded best-effort.
153
+
154
+ ## Callbacks
155
+
156
+ `onSubmit(submission)` fires the moment the server accepts the request
157
+ (`status: 'pending'`). `onError(KYCError)` fires for **technical** errors only
158
+ (`network_error`, `invalid_api_key`, `insufficient_credits`, `upload_failed`,
159
+ `camera_permission_denied`, `feature_disabled`). Verification *outcomes* arrive
160
+ asynchronously via webhook / `GET /api/kyc/status/:id` — never through `onError`.
161
+
162
+ ## License
163
+
164
+ UNLICENSED — © Myaza.
@@ -0,0 +1,29 @@
1
+ project(KycSdkReactNative)
2
+ cmake_minimum_required(VERSION 3.9.0)
3
+
4
+ set(PACKAGE_NAME KycSdkReactNative)
5
+ set(CMAKE_CXX_STANDARD 20)
6
+
7
+ # Our only hand-written C++: the JNI_OnLoad that registers the Nitro HybridObjects.
8
+ add_library(${PACKAGE_NAME} SHARED
9
+ src/main/cpp/cpp-adapter.cpp
10
+ )
11
+
12
+ # Add the nitrogen-generated specs + autolinking (adds sources, includes, and links
13
+ # fbjni / ReactAndroid / react-native-nitro-modules to ${PACKAGE_NAME}).
14
+ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/KycSdkReactNative+autolinking.cmake)
15
+
16
+ # Our Nitro spec's `Frame` type comes from VisionCamera, so the generated C++
17
+ # includes <VisionCamera/HybridFrameSpec.hpp> — link VisionCamera's prefab to put
18
+ # its headers on the include path. (Nitrogen doesn't auto-link external-module
19
+ # prefabs, so we add it by hand.)
20
+ find_package(react-native-vision-camera REQUIRED CONFIG)
21
+
22
+ find_library(LOG_LIB log)
23
+
24
+ target_link_libraries(
25
+ ${PACKAGE_NAME}
26
+ ${LOG_LIB}
27
+ android
28
+ react-native-vision-camera::VisionCamera
29
+ )
@@ -0,0 +1,110 @@
1
+ // Gradle build for the Myaza KYC RN SDK's Android native piece — the Google ML Kit
2
+ // face detector, now a VisionCamera v5 **Nitro** HybridObject (was a v4
3
+ // FrameProcessorPlugin). Mirrors react-native-nitro-modules' Android build pattern:
4
+ // the nitrogen-generated C++ specs are compiled into a shared lib (KycSdkReactNative)
5
+ // via CMake, and the Kotlin impl (HybridMyazaFaceDetector) backs it.
6
+ //
7
+ // ML Kit stays Android-only (Gradle) — no GoogleMLKit CocoaPod on iOS — so the SDK
8
+ // still builds on Apple-Silicon iOS simulators (iOS uses Apple Vision).
9
+
10
+ buildscript {
11
+ ext.kotlin_version = ext.has('kotlinVersion') ? ext.kotlinVersion : '1.9.24'
12
+ repositories {
13
+ google()
14
+ mavenCentral()
15
+ }
16
+ dependencies {
17
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
18
+ }
19
+ }
20
+
21
+ def reactNativeArchitectures() {
22
+ def value = rootProject.getProperties().get("reactNativeArchitectures")
23
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
24
+ }
25
+
26
+ apply plugin: 'com.android.library'
27
+ apply plugin: 'kotlin-android'
28
+ // Adds the nitrogen-generated Kotlin sources (HybridMyazaFaceDetectorSpec, FaceResult, …).
29
+ apply from: '../nitrogen/generated/android/KycSdkReactNative+autolinking.gradle'
30
+
31
+ def safeExtGet(prop, fallback) {
32
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
33
+ }
34
+
35
+ android {
36
+ namespace 'co.myazahq.kyc.rn'
37
+ compileSdk safeExtGet('compileSdkVersion', 34)
38
+ ndkVersion safeExtGet('ndkVersion', '27.1.12297006')
39
+
40
+ defaultConfig {
41
+ minSdk safeExtGet('minSdkVersion', 24)
42
+ targetSdk safeExtGet('targetSdkVersion', 34)
43
+
44
+ externalNativeBuild {
45
+ cmake {
46
+ cppFlags "-frtti -fexceptions -Wall -Wextra -fstack-protector-all"
47
+ arguments "-DANDROID_STL=c++_shared"
48
+ abiFilters(*reactNativeArchitectures())
49
+ }
50
+ }
51
+ }
52
+
53
+ externalNativeBuild {
54
+ cmake {
55
+ path "CMakeLists.txt"
56
+ }
57
+ }
58
+
59
+ buildFeatures {
60
+ prefab true
61
+ buildConfig true
62
+ }
63
+
64
+ // Don't bundle the shared libs already provided by the host app / RN.
65
+ packagingOptions {
66
+ excludes = [
67
+ "META-INF",
68
+ "META-INF/**",
69
+ "**/libc++_shared.so",
70
+ "**/libfbjni.so",
71
+ "**/libjsi.so",
72
+ "**/libreactnative.so",
73
+ "**/libreactnativejni.so",
74
+ ]
75
+ }
76
+
77
+ compileOptions {
78
+ sourceCompatibility JavaVersion.VERSION_17
79
+ targetCompatibility JavaVersion.VERSION_17
80
+ }
81
+
82
+ kotlinOptions {
83
+ jvmTarget = '17'
84
+ }
85
+ }
86
+
87
+ repositories {
88
+ google()
89
+ mavenCentral()
90
+ }
91
+
92
+ dependencies {
93
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
94
+ implementation 'com.facebook.react:react-android'
95
+
96
+ // Nitro core — provides the HybridObject base + the NitroModules prefab the
97
+ // generated CMake links against (find_package(react-native-nitro-modules)).
98
+ implementation project(':react-native-nitro-modules')
99
+
100
+ // VisionCamera — provides the Nitro `HybridFrameSpec` + `NativeFrame` (ImageProxy)
101
+ // the detector reads, plus its C++ prefab (headers for the generated spec).
102
+ // `implementation` (not compileOnly) so the prefab is consumable by our CMake.
103
+ implementation project(':react-native-vision-camera')
104
+
105
+ // CameraX core — for the ImageProxy / ExperimentalGetImage APIs.
106
+ compileOnly 'androidx.camera:camera-core:1.3.4'
107
+
108
+ // Google ML Kit face detection — Android only. Same version as the Flutter SDK.
109
+ implementation 'com.google.mlkit:face-detection:16.1.6'
110
+ }
@@ -0,0 +1,12 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <!--
3
+ The ML Kit face-detection model is downloaded on demand by Play Services.
4
+ This meta-data tells Play Services to fetch it at install time so the first
5
+ liveness session doesn't pay the download cost. Mirrors the Flutter SDK.
6
+ -->
7
+ <application>
8
+ <meta-data
9
+ android:name="com.google.mlkit.vision.DEPENDENCIES"
10
+ android:value="face" />
11
+ </application>
12
+ </manifest>
@@ -0,0 +1,12 @@
1
+ #include <jni.h>
2
+ #include <fbjni/fbjni.h>
3
+ #include "KycSdkReactNativeOnLoad.hpp"
4
+
5
+ // Loaded via System.loadLibrary("KycSdkReactNative") (KycSdkReactNativeOnLoad.kt).
6
+ // Registers all nitrogen-generated HybridObjects (MyazaFaceDetector) so the JS
7
+ // `NitroModules.createHybridObject('MyazaFaceDetector')` resolves to the Kotlin impl.
8
+ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
9
+ return facebook::jni::initialize(vm, []() {
10
+ margelo::nitro::myazakyc::registerAllNatives();
11
+ });
12
+ }
@@ -0,0 +1,36 @@
1
+ package co.myazahq.kyc.rn
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+ import com.margelo.nitro.myazakyc.KycSdkReactNativeOnLoad
8
+
9
+ /**
10
+ * Loads the SDK's Nitro C++ library (`libKycSdkReactNative.so`) at app startup.
11
+ * The library's JNI_OnLoad (`src/main/cpp/cpp-adapter.cpp`) registers the
12
+ * MyazaFaceDetector HybridObject, so JS
13
+ * `NitroModules.createHybridObject('MyazaFaceDetector')` resolves to the Kotlin
14
+ * impl (`com.margelo.nitro.myazakyc.HybridMyazaFaceDetector`).
15
+ *
16
+ * VisionCamera v5 dropped the FrameProcessorPluginRegistry model — Nitro
17
+ * HybridObjects replace it — so this package no longer registers a frame
18
+ * processor. It exists only so RN CLI autolinking (`react-native.config.js`)
19
+ * instantiates it on the host app's PackageList, triggering the library load
20
+ * (and therefore the HybridObject registration) before the liveness screen runs.
21
+ * It exposes no React Native modules or views.
22
+ */
23
+ class MyazaFaceDetectorPackage : ReactPackage {
24
+ init {
25
+ KycSdkReactNativeOnLoad.initializeNative()
26
+ }
27
+
28
+ // MyazaStatusBarModule themes the modal's (Dialog window's) status/nav bars —
29
+ // something JS StatusBar/SystemBars can't reach. A native module (not a view
30
+ // manager) because legacy view managers aren't instantiated under Fabric.
31
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
32
+ listOf(MyazaStatusBarModule(reactContext))
33
+
34
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
35
+ emptyList()
36
+ }
@@ -0,0 +1,64 @@
1
+ package co.myazahq.kyc.rn
2
+
3
+ import android.util.Log
4
+ import androidx.core.view.ViewCompat
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
7
+ import com.facebook.react.bridge.ReactMethod
8
+ import com.facebook.react.bridge.UiThreadUtil
9
+ import com.facebook.react.uimanager.UIManagerHelper
10
+
11
+ /**
12
+ * Themes the status/navigation-bar ICON appearance of the SDK modal's window.
13
+ *
14
+ * The SDK flow renders inside a React Native `<Modal>`, which runs in its own
15
+ * native **Dialog window**. JS `StatusBar`/`SystemBars` only reach the **activity**
16
+ * window, so they can't theme the modal's bars. A native **view-manager** can't
17
+ * either — on the New Architecture, Fabric renders unregistered legacy view
18
+ * managers as a blank "unimplemented view". Native **modules**, however, work fine
19
+ * on Fabric, so this module resolves a view by tag (a view the SDK rendered INSIDE
20
+ * the modal) and applies the bar appearance to THAT view's window — i.e. the Dialog.
21
+ */
22
+ class MyazaStatusBarModule(reactContext: ReactApplicationContext) :
23
+ ReactContextBaseJavaModule(reactContext) {
24
+
25
+ override fun getName(): String = NAME
26
+
27
+ /**
28
+ * @param viewTag a React tag of a view rendered inside the modal (its window is
29
+ * the modal Dialog).
30
+ * @param light true → light glyphs (dark background); false → dark glyphs.
31
+ */
32
+ @ReactMethod
33
+ fun setBarStyle(viewTag: Double, light: Boolean) {
34
+ val tag = viewTag.toInt()
35
+ val ctx = reactApplicationContext
36
+ UiThreadUtil.runOnUiThread {
37
+ val view = try {
38
+ UIManagerHelper.getUIManagerForReactTag(ctx, tag)?.resolveView(tag)
39
+ } catch (e: Exception) {
40
+ Log.w(TAG, "resolveView($tag) failed: ${e.message}")
41
+ null
42
+ } ?: ctx.currentActivity?.window?.decorView
43
+
44
+ if (view == null) {
45
+ Log.w(TAG, "no view/window to theme")
46
+ return@runOnUiThread
47
+ }
48
+
49
+ val controller = ViewCompat.getWindowInsetsController(view)
50
+ if (controller == null) {
51
+ Log.w(TAG, "insets controller is null for tag=$tag")
52
+ return@runOnUiThread
53
+ }
54
+ // isAppearanceLightStatusBars = true → dark glyphs; false → light glyphs.
55
+ controller.isAppearanceLightStatusBars = !light
56
+ controller.isAppearanceLightNavigationBars = !light
57
+ }
58
+ }
59
+
60
+ companion object {
61
+ const val NAME = "MyazaStatusBarModule"
62
+ private const val TAG = "MyazaStatusBar"
63
+ }
64
+ }
@@ -0,0 +1,134 @@
1
+ package com.margelo.nitro.myazakyc
2
+
3
+ import androidx.camera.core.ExperimentalGetImage
4
+ import androidx.camera.core.ImageProxy
5
+ import com.margelo.nitro.camera.HybridFrameSpec
6
+ import com.margelo.nitro.camera.public.NativeFrame
7
+ import com.google.mlkit.vision.common.InputImage
8
+ import com.google.mlkit.vision.face.Face
9
+ import com.google.mlkit.vision.face.FaceDetection
10
+ import com.google.mlkit.vision.face.FaceDetector
11
+ import com.google.mlkit.vision.face.FaceDetectorOptions
12
+ import java.util.concurrent.CountDownLatch
13
+ import java.util.concurrent.TimeUnit
14
+
15
+ /**
16
+ * Android half of the Myaza KYC on-device face detector — now a VisionCamera v5
17
+ * **Nitro** HybridObject (the v4 FrameProcessorPlugin + FrameProcessorPluginRegistry
18
+ * model is gone). Implements the nitrogen-generated `HybridMyazaFaceDetectorSpec`
19
+ * from src/specs/MyazaFaceDetector.nitro.ts. The iOS half is
20
+ * HybridMyazaFaceDetector.swift (Apple Vision). Both return the SAME `FaceResult`
21
+ * shape the TS liveness flow consumes (see liveness/types.ts).
22
+ *
23
+ * Google ML Kit returns head pose + smile + eye-open DIRECTLY (no landmark math),
24
+ * so these map 1:1 onto LivenessFaceData and need no device tuning — unlike the iOS
25
+ * Vision heuristics. Direct port of the old v4 plugin's ML Kit logic, plus the
26
+ * frame-brightness signal (for the low-light gate) computed like the iOS side.
27
+ *
28
+ * The Nitro worklet calls detectFace synchronously, so we run ML Kit's async
29
+ * detector and await the result on the worklet thread with a short bound (FAST mode
30
+ * is well under a frame interval, so this doesn't stall the camera pipeline).
31
+ */
32
+ class HybridMyazaFaceDetector : HybridMyazaFaceDetectorSpec() {
33
+ private val detector: FaceDetector = FaceDetection.getClient(
34
+ FaceDetectorOptions.Builder()
35
+ .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
36
+ .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
37
+ .enableTracking()
38
+ .setMinFaceSize(0.15f)
39
+ .build(),
40
+ )
41
+
42
+ @ExperimentalGetImage
43
+ override fun detectFace(frame: HybridFrameSpec): FaceResult {
44
+ // Same access pattern as iOS (cast to the public NativeFrame): VisionCamera's
45
+ // frame → CameraX ImageProxy → the underlying android.media.Image for ML Kit.
46
+ val proxy: ImageProxy = (frame as? NativeFrame)?.image ?: return noFace(128.0)
47
+ val brightness = averageLuma(proxy)
48
+ val mediaImage = proxy.image ?: return noFace(brightness)
49
+
50
+ val rotation = proxy.imageInfo.rotationDegrees
51
+ val input = InputImage.fromMediaImage(mediaImage, rotation)
52
+
53
+ var result: FaceResult? = null
54
+ val latch = CountDownLatch(1)
55
+ detector.process(input)
56
+ .addOnSuccessListener { faces ->
57
+ val face = faces.maxByOrNull { it.boundingBox.width() * it.boundingBox.height() }
58
+ result = face?.let { toResult(it, mediaImage.width, mediaImage.height, rotation, faces.size, brightness) }
59
+ latch.countDown()
60
+ }
61
+ .addOnFailureListener { latch.countDown() }
62
+
63
+ latch.await(200, TimeUnit.MILLISECONDS)
64
+ return result ?: noFace(brightness)
65
+ }
66
+
67
+ private fun toResult(
68
+ face: Face,
69
+ imageWidth: Int,
70
+ imageHeight: Int,
71
+ rotation: Int,
72
+ faceCount: Int,
73
+ brightness: Double,
74
+ ): FaceResult {
75
+ // After rotation the frame is upright; when rotated 90/270 the stored width/
76
+ // height are swapped, so the smaller dimension is the upright "width".
77
+ val frameWidth = if (rotation == 90 || rotation == 270) imageHeight else imageWidth
78
+ val ratio = (face.boundingBox.width().toDouble() / frameWidth).coerceIn(0.0, 1.0)
79
+ return FaceResult(
80
+ headEulerAngleX = face.headEulerAngleX.toDouble(),
81
+ headEulerAngleY = face.headEulerAngleY.toDouble(),
82
+ headEulerAngleZ = face.headEulerAngleZ.toDouble(),
83
+ smilingProbability = face.smilingProbability?.toDouble() ?: 0.0,
84
+ leftEyeOpenProbability = face.leftEyeOpenProbability?.toDouble() ?: 0.0,
85
+ rightEyeOpenProbability = face.rightEyeOpenProbability?.toDouble() ?: 0.0,
86
+ faceSizeRatio = ratio,
87
+ faceCount = faceCount.toDouble(),
88
+ brightness = brightness,
89
+ )
90
+ }
91
+
92
+ /** Mean luma (0–255) sampled on a coarse ~64×48 grid from the Y plane. */
93
+ private fun averageLuma(image: ImageProxy): Double {
94
+ val plane = image.planes.getOrNull(0) ?: return 128.0
95
+ val buffer = plane.buffer
96
+ val rowStride = plane.rowStride
97
+ val pixelStride = plane.pixelStride
98
+ val w = image.width
99
+ val h = image.height
100
+ val stepX = maxOf(1, w / 64)
101
+ val stepY = maxOf(1, h / 48)
102
+ var sum = 0.0
103
+ var count = 0
104
+ var y = 0
105
+ while (y < h) {
106
+ val rowStart = y * rowStride
107
+ var x = 0
108
+ while (x < w) {
109
+ val idx = rowStart + x * pixelStride
110
+ if (idx < buffer.limit()) {
111
+ sum += (buffer.get(idx).toInt() and 0xFF)
112
+ count++
113
+ }
114
+ x += stepX
115
+ }
116
+ y += stepY
117
+ }
118
+ return if (count > 0) sum / count else 128.0
119
+ }
120
+
121
+ // "No face" sentinel — faceCount 0 (the worklet maps this to onNoFace), still
122
+ // carrying the frame brightness so the low-light gate works pre-face.
123
+ private fun noFace(brightness: Double) = FaceResult(
124
+ headEulerAngleX = 0.0,
125
+ headEulerAngleY = 0.0,
126
+ headEulerAngleZ = 0.0,
127
+ smilingProbability = 0.0,
128
+ leftEyeOpenProbability = 1.0,
129
+ rightEyeOpenProbability = 1.0,
130
+ faceSizeRatio = 0.0,
131
+ faceCount = 0.0,
132
+ brightness = brightness,
133
+ )
134
+ }
package/app.plugin.js ADDED
@@ -0,0 +1,55 @@
1
+ const { withInfoPlist, withAndroidManifest, createRunOncePlugin, AndroidConfig } = require('expo/config-plugins');
2
+
3
+ const pkg = require('./package.json');
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Expo config plugin for @myazahq/kyc-sdk-react-native.
7
+ //
8
+ // The native face-detector frame-processor plugin (iOS Apple Vision / Android
9
+ // ML Kit) is autolinked via expo-module.config.json (the podspec + the Android
10
+ // Gradle project + MyazaFaceDetectorPackage). This config plugin only handles
11
+ // the things that must be written into the HOST app's native config:
12
+ //
13
+ // • iOS — NSCameraUsageDescription (document scan + selfie liveness).
14
+ // NOTE: voice guidance is TTS OUTPUT only, so NO microphone usage
15
+ // string and no NSMicrophoneUsageDescription are added.
16
+ // • Android — CAMERA + INTERNET permissions.
17
+ //
18
+ // Camera *preview/capture* itself comes from react-native-vision-camera; add its
19
+ // own config plugin too (it injects the VisionCamera frame-processor build flag).
20
+ // This plugin is intentionally minimal and idempotent (createRunOncePlugin).
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const CAMERA_USAGE =
24
+ 'We use the camera to photograph your ID document and to capture a live selfie for identity verification.';
25
+
26
+ function withIosCameraUsage(config, customMessage) {
27
+ return withInfoPlist(config, (cfg) => {
28
+ if (!cfg.modResults.NSCameraUsageDescription) {
29
+ cfg.modResults.NSCameraUsageDescription = customMessage || CAMERA_USAGE;
30
+ }
31
+ return cfg;
32
+ });
33
+ }
34
+
35
+ function withAndroidCameraPermissions(config) {
36
+ return withAndroidManifest(config, (cfg) => {
37
+ AndroidConfig.Permissions.ensurePermissions(cfg.modResults, [
38
+ 'android.permission.CAMERA',
39
+ 'android.permission.INTERNET',
40
+ ]);
41
+ return cfg;
42
+ });
43
+ }
44
+
45
+ /**
46
+ * @param {object} config
47
+ * @param {{ cameraPermission?: string }} [props]
48
+ */
49
+ function withMyazaKyc(config, props = {}) {
50
+ config = withIosCameraUsage(config, props.cameraPermission);
51
+ config = withAndroidCameraPermissions(config);
52
+ return config;
53
+ }
54
+
55
+ module.exports = createRunOncePlugin(withMyazaKyc, pkg.name, pkg.version);