@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.
- package/KycSdkReactNative.podspec +32 -0
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/android/CMakeLists.txt +29 -0
- package/android/build.gradle +110 -0
- package/android/src/main/AndroidManifest.xml +12 -0
- package/android/src/main/cpp/cpp-adapter.cpp +12 -0
- package/android/src/main/java/co/myazahq/kyc/rn/MyazaFaceDetectorPackage.kt +36 -0
- package/android/src/main/java/co/myazahq/kyc/rn/MyazaStatusBarModule.kt +64 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +134 -0
- package/app.plugin.js +55 -0
- package/expo-module.config.json +6 -0
- package/ios/HybridMyazaFaceDetector.swift +233 -0
- package/package.json +84 -0
- package/react-native.config.js +23 -0
- package/src/MyazaKYC.tsx +235 -0
- package/src/__tests__/cardCrop.test.ts +39 -0
- package/src/__tests__/deviceMetadata.test.ts +34 -0
- package/src/__tests__/errors.test.ts +34 -0
- package/src/__tests__/flow.test.ts +61 -0
- package/src/__tests__/gestureDetector.test.ts +37 -0
- package/src/__tests__/liveness.test.ts +112 -0
- package/src/__tests__/resolveUrl.test.ts +64 -0
- package/src/__tests__/validators.test.ts +38 -0
- package/src/assets/liveness/Blink.gif +0 -0
- package/src/assets/liveness/Nod.gif +0 -0
- package/src/assets/liveness/Smile.gif +0 -0
- package/src/assets/liveness/Turn.gif +0 -0
- package/src/components/CameraPermissionView.tsx +116 -0
- package/src/components/CameraViewfinder.tsx +156 -0
- package/src/components/CountryFlag.tsx +52 -0
- package/src/components/DocumentCropper.tsx +325 -0
- package/src/components/GlassIconButton.tsx +92 -0
- package/src/components/Icon.tsx +125 -0
- package/src/components/KycFlow.tsx +205 -0
- package/src/components/KycSheet.tsx +224 -0
- package/src/components/MyazaAlert.tsx +57 -0
- package/src/components/MyazaButton.tsx +101 -0
- package/src/components/MyazaCard.tsx +48 -0
- package/src/components/MyazaInput.tsx +112 -0
- package/src/components/MyazaPulseLoader.tsx +71 -0
- package/src/components/StatusBarController.tsx +42 -0
- package/src/components/StepHeader.tsx +56 -0
- package/src/components/StepIndicator.tsx +74 -0
- package/src/components/Typography.tsx +69 -0
- package/src/components/fonts.ts +49 -0
- package/src/components/glass/GlassGroup.tsx +34 -0
- package/src/components/glass/GlassSurface.tsx +64 -0
- package/src/components/runtime.tsx +93 -0
- package/src/components/toast.tsx +154 -0
- package/src/components/useBranding.ts +27 -0
- package/src/components/useVideoRecorder.ts +122 -0
- package/src/config/captureSettings.ts +67 -0
- package/src/config/idTypes.ts +79 -0
- package/src/config/theme.ts +186 -0
- package/src/index.ts +54 -0
- package/src/liveness/challengeManager.ts +130 -0
- package/src/liveness/faceDetector.ts +79 -0
- package/src/liveness/gestureDetector.ts +80 -0
- package/src/liveness/speech.ts +66 -0
- package/src/liveness/types.ts +99 -0
- package/src/liveness/useLiveness.ts +484 -0
- package/src/liveness/visionCameraFaceDetector.ts +118 -0
- package/src/screens/ConsentStep.tsx +164 -0
- package/src/screens/DocumentCaptureStep.tsx +500 -0
- package/src/screens/IdInputStep.tsx +79 -0
- package/src/screens/IdTypeStep.tsx +142 -0
- package/src/screens/LivenessAvatar.tsx +69 -0
- package/src/screens/LivenessStep.tsx +615 -0
- package/src/screens/SubmittedStep.tsx +177 -0
- package/src/services/api.ts +291 -0
- package/src/services/cardCrop.ts +52 -0
- package/src/services/deviceMetadata.ts +185 -0
- package/src/services/errors.ts +92 -0
- package/src/services/mediaCompress.ts +129 -0
- package/src/services/resolveUrl.ts +98 -0
- package/src/services/retry.ts +70 -0
- package/src/services/validators.ts +103 -0
- package/src/specs/MyazaFaceDetector.nitro.ts +44 -0
- package/src/store/kycStore.ts +288 -0
- package/src/store/serverConfig.ts +78 -0
- package/src/types/config.ts +239 -0
- package/src/types/country-flag-icons.d.ts +6 -0
- package/src/types/verification.ts +79 -0
- package/src/utils/platform.ts +23 -0
- package/src/utils/tokens.ts +10 -0
- 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);
|