@myazahq/kyc-sdk-react-native 2.0.1 → 2.1.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 +111 -8
- package/android/build.gradle +6 -0
- package/android/src/main/AndroidManifest.xml +14 -4
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaEmrtd.kt +358 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +88 -1
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaRectDetector.kt +34 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +118 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/Jp2Decoder.kt +117 -0
- package/app.plugin.js +102 -13
- package/ios/HybridMyazaEmrtd.swift +447 -0
- package/ios/HybridMyazaFaceDetector.swift +104 -2
- package/ios/HybridMyazaRectDetector.swift +103 -0
- package/ios/HybridMyazaTextRecognizer.swift +111 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/KycSdkReactNative+autolinking.cmake +87 -0
- package/nitrogen/generated/android/KycSdkReactNative+autolinking.gradle +27 -0
- package/nitrogen/generated/android/KycSdkReactNativeOnLoad.cpp +102 -0
- package/nitrogen/generated/android/KycSdkReactNativeOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JDetectedRect.hpp +81 -0
- package/nitrogen/generated/android/c++/JEmrtdApduResponse.hpp +61 -0
- package/nitrogen/generated/android/c++/JEmrtdTagInfo.hpp +61 -0
- package/nitrogen/generated/android/c++/JFaceResult.hpp +113 -0
- package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.cpp +176 -0
- package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +60 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +63 -0
- package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.cpp +60 -0
- package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.hpp +63 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +80 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +64 -0
- package/nitrogen/generated/android/c++/JTextResult.hpp +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/DetectedRect.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdApduResponse.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdTagInfo.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/FaceResult.kt +121 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaEmrtdSpec.kt +103 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +55 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaRectDetectorSpec.kt +55 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +60 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/KycSdkReactNativeOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/TextResult.kt +51 -0
- package/nitrogen/generated/ios/KycSdkReactNative+autolinking.rb +62 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +143 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +423 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Umbrella.hpp +81 -0
- package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.mm +57 -0
- package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.swift +62 -0
- package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.hpp +184 -0
- package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +87 -0
- package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.hpp +87 -0
- package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +98 -0
- package/nitrogen/generated/ios/swift/DetectedRect.swift +59 -0
- package/nitrogen/generated/ios/swift/EmrtdApduResponse.swift +34 -0
- package/nitrogen/generated/ios/swift/EmrtdTagInfo.swift +34 -0
- package/nitrogen/generated/ios/swift/FaceResult.swift +99 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_EmrtdApduResponse.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_EmrtdTagInfo.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_TextResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec.swift +67 -0
- package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec_cxx.swift +317 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +143 -0
- package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec_cxx.swift +143 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +57 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +162 -0
- package/nitrogen/generated/ios/swift/TextResult.swift +35 -0
- package/nitrogen/generated/shared/c++/DetectedRect.hpp +107 -0
- package/nitrogen/generated/shared/c++/EmrtdApduResponse.hpp +87 -0
- package/nitrogen/generated/shared/c++/EmrtdTagInfo.hpp +87 -0
- package/nitrogen/generated/shared/c++/FaceResult.hpp +139 -0
- package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.cpp +33 -0
- package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.hpp +80 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +67 -0
- package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.hpp +67 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +70 -0
- package/nitrogen/generated/shared/c++/TextResult.hpp +84 -0
- package/package.json +16 -4
- package/src/MyazaKYC.tsx +115 -22
- package/src/capture/documentIdentity.ts +153 -0
- package/src/capture/documentSignals.ts +177 -0
- package/src/capture/documentTextGate.ts +171 -0
- package/src/capture/hints.ts +57 -0
- package/src/capture/index.ts +24 -0
- package/src/capture/rectDetector.ts +84 -0
- package/src/capture/rectGate.ts +91 -0
- package/src/capture/useAutoCapture.ts +198 -0
- package/src/components/CameraPermissionView.tsx +39 -3
- package/src/components/CameraViewfinder.tsx +226 -70
- package/src/components/ContactCodeEntry.tsx +139 -0
- package/src/components/CountryField.tsx +87 -0
- package/src/components/CountryFlag.tsx +15 -15
- package/src/components/DialCodePicker.tsx +186 -0
- package/src/components/DocumentCropper.tsx +1 -1
- package/src/components/DocumentReview.tsx +142 -0
- package/src/components/DocumentReviewSide.tsx +135 -0
- package/src/components/DocumentReviewZoom.tsx +119 -0
- package/src/components/ExpiryCountdown.tsx +52 -0
- package/src/components/FlashOverlay.tsx +156 -0
- package/src/components/Icon.tsx +67 -1
- package/src/components/KycFlow.tsx +66 -94
- package/src/components/KycSheet.tsx +148 -18
- package/src/components/MediaSourceSheet.tsx +163 -0
- package/src/components/MyazaButton.tsx +14 -2
- package/src/components/MyazaDateField.tsx +240 -0
- package/src/components/MyazaInput.tsx +73 -26
- package/src/components/MyazaSelect.tsx +283 -0
- package/src/components/MyazaWordmark.tsx +33 -0
- package/src/components/OptionRow.tsx +132 -0
- package/src/components/PhoneNumberInput.tsx +149 -0
- package/src/components/PoweredBy.tsx +89 -0
- package/src/components/ReadyPrimer.tsx +149 -0
- package/src/components/StepView.tsx +91 -0
- package/src/components/Typography.tsx +18 -3
- package/src/components/VerifiedNotice.tsx +38 -0
- package/src/components/WhatsAppIcon.tsx +29 -0
- package/src/components/WorkflowGate.tsx +128 -0
- package/src/components/brand-font.ts +113 -0
- package/src/components/flashHoleGeometry.ts +35 -0
- package/src/components/flow/FatalConfigError.tsx +56 -0
- package/src/components/fonts.ts +2 -19
- package/src/components/readyPrimerContent.ts +52 -0
- package/src/components/runtime.tsx +38 -43
- package/src/components/stepHeaderMeta.tsx +87 -0
- package/src/components/theme-provider.tsx +89 -0
- package/src/components/useWorkflowMount.ts +116 -0
- package/src/components/viewfinder/CardGuide.tsx +60 -0
- package/src/components/viewfinder/DocumentGhost.tsx +149 -0
- package/src/components/viewfinder/ImmersiveBottomBar.tsx +172 -0
- package/src/components/viewfinder/ImmersiveControls.tsx +127 -0
- package/src/components/viewfinder/ImmersiveGuide.tsx +174 -0
- package/src/components/viewfinder/ImmersiveOverlay.tsx +79 -0
- package/src/components/viewfinder/ViewfinderControls.tsx +154 -0
- package/src/config/brand.ts +97 -0
- package/src/config/business.ts +179 -0
- package/src/config/businessSteps.ts +105 -0
- package/src/config/contact.ts +159 -0
- package/src/config/countryNames.g.ts +254 -0
- package/src/config/currencyFlags.ts +144 -0
- package/src/config/font-resolve.ts +78 -0
- package/src/config/idTypes.ts +98 -1
- package/src/config/keyPeople.ts +152 -0
- package/src/config/phone.ts +39 -0
- package/src/config/proofOfAddress.ts +75 -0
- package/src/config/questionnaire.ts +153 -0
- package/src/config/regions.ts +118 -0
- package/src/config/registrationHint.ts +87 -0
- package/src/config/stepOrder.ts +118 -0
- package/src/config/theme.ts +38 -2
- package/src/config/workflowMerge.ts +124 -0
- package/src/emrtd/bac.ts +128 -0
- package/src/emrtd/bytes.ts +127 -0
- package/src/emrtd/crypto.ts +163 -0
- package/src/emrtd/der.ts +192 -0
- package/src/emrtd/dg2.ts +111 -0
- package/src/emrtd/files.ts +215 -0
- package/src/emrtd/index.ts +30 -0
- package/src/emrtd/mrzKey.ts +54 -0
- package/src/emrtd/native.ts +79 -0
- package/src/emrtd/optionalRead.ts +42 -0
- package/src/emrtd/read.ts +224 -0
- package/src/emrtd/retry.ts +98 -0
- package/src/emrtd/secureMessaging.ts +178 -0
- package/src/emrtd/session.ts +200 -0
- package/src/emrtd/stages.ts +122 -0
- package/src/index.ts +110 -0
- package/src/lib/calendar.ts +71 -0
- package/src/lib/use-keyboard-inset.ts +34 -0
- package/src/liveness/challengeManager.ts +27 -1
- package/src/liveness/faceContinuity.ts +153 -0
- package/src/liveness/flashDetector.ts +145 -0
- package/src/liveness/flashReadyGate.ts +106 -0
- package/src/liveness/flashRunner.ts +118 -0
- package/src/liveness/integritySignals.ts +60 -0
- package/src/liveness/livenessState.ts +139 -0
- package/src/liveness/speech.ts +19 -1
- package/src/liveness/types.ts +48 -1
- package/src/liveness/useFlashPhase.ts +78 -0
- package/src/liveness/useLiveness.ts +260 -75
- package/src/liveness/visionCameraFaceDetector.ts +14 -0
- package/src/mrz/extract.ts +101 -0
- package/src/mrz/parse.ts +178 -0
- package/src/mrz/textRecognizer.ts +149 -0
- package/src/screens/ApplicantRoleStep.tsx +331 -0
- package/src/screens/BusinessDetailsStep.tsx +212 -0
- package/src/screens/BusinessDocumentSlot.tsx +167 -0
- package/src/screens/BusinessDocumentsStep.tsx +213 -0
- package/src/screens/BusinessKeyPeopleStep.tsx +151 -0
- package/src/screens/BusinessKeyPersonRow.tsx +170 -0
- package/src/screens/CompanyInfoFields.tsx +83 -0
- package/src/screens/ConsentStep.tsx +68 -68
- package/src/screens/ContactChannelChoice.tsx +133 -0
- package/src/screens/ContactCodeStep.tsx +130 -0
- package/src/screens/ContactDestinationField.tsx +70 -0
- package/src/screens/ContactFooterNote.tsx +34 -0
- package/src/screens/ContactVerificationStep.tsx +199 -0
- package/src/screens/ContactVerifiedPanel.tsx +36 -0
- package/src/screens/CountrySelectStep.tsx +210 -0
- package/src/screens/DocumentCaptureStep.tsx +190 -145
- package/src/screens/IdInputStep.tsx +19 -9
- package/src/screens/IdTypeStep.tsx +74 -33
- package/src/screens/KeyPeopleInviteLinks.tsx +239 -0
- package/src/screens/LivenessStep.tsx +181 -267
- package/src/screens/MrzScanView.tsx +173 -0
- package/src/screens/NfcStep.tsx +290 -0
- package/src/screens/ProofOfAddressStep.tsx +238 -0
- package/src/screens/QuestionnaireField.tsx +219 -0
- package/src/screens/QuestionnaireMoneyField.tsx +134 -0
- package/src/screens/QuestionnaireStep.tsx +76 -0
- package/src/screens/SubmittedStep.tsx +5 -0
- package/src/screens/consent/model.ts +109 -0
- package/src/screens/document/CameraPhase.tsx +150 -0
- package/src/screens/document/RequiredPill.tsx +60 -0
- package/src/screens/liveness/LightingBanner.tsx +50 -0
- package/src/screens/liveness/LivenessOutcome.tsx +109 -0
- package/src/screens/liveness/ProgressDots.tsx +67 -0
- package/src/screens/liveness/SelfiePreview.tsx +39 -0
- package/src/screens/liveness/constants.ts +31 -0
- package/src/screens/liveness/index.ts +8 -0
- package/src/screens/liveness/resolveGuidance.ts +27 -0
- package/src/screens/liveness/useSelfieUpload.ts +107 -0
- package/src/screens/nfc/NfcMrzPrompt.tsx +40 -0
- package/src/screens/nfc/NfcReadBody.tsx +63 -0
- package/src/screens/nfc/NfcReadProgress.tsx +96 -0
- package/src/screens/nfc/NfcScanIllustration.tsx +244 -0
- package/src/screens/nfc/NfcScannedSummary.tsx +74 -0
- package/src/screens/nfc/NfcSuccessPanel.tsx +162 -0
- package/src/screens/nfc/availability.ts +73 -0
- package/src/screens/nfc/readErrorMessage.ts +93 -0
- package/src/screens/nfc/useChipPortrait.ts +58 -0
- package/src/screens/nfc/useNfcAvailability.ts +82 -0
- package/src/screens/useFlashHole.ts +105 -0
- package/src/screens/useFlashSequence.ts +96 -0
- package/src/screens/usePoaAttach.ts +121 -0
- package/src/services/api-types.ts +164 -0
- package/src/services/api-verify-types.ts +99 -0
- package/src/services/api.ts +72 -102
- package/src/services/cardCrop.ts +25 -10
- package/src/services/contactErrors.ts +43 -0
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/documentPicker.ts +20 -0
- package/src/services/fingerprint-sources.ts +129 -0
- package/src/services/fingerprint.ts +101 -0
- package/src/services/haptics.ts +38 -0
- package/src/services/mediaCompress.ts +7 -2
- package/src/services/resolveUrl.ts +3 -2
- package/src/services/validators.ts +3 -0
- package/src/services/workflowGate.ts +138 -0
- package/src/specs/MyazaEmrtd.nitro.ts +114 -0
- package/src/specs/MyazaFaceDetector.nitro.ts +10 -0
- package/src/specs/MyazaRectDetector.nitro.ts +50 -0
- package/src/specs/MyazaTextRecognizer.nitro.ts +48 -0
- package/src/store/derive.ts +225 -0
- package/src/store/kycStore.ts +250 -149
- package/src/store/state.ts +253 -0
- package/src/store/submit.ts +101 -0
- package/src/store/submitApplicant.ts +81 -0
- package/src/types/appearance.ts +113 -0
- package/src/types/business.ts +131 -0
- package/src/types/config.ts +150 -119
- package/src/types/id-types.ts +57 -0
- package/src/types/verification.ts +5 -0
- package/src/types/workflow.ts +141 -0
- package/src/utils/amountFormat.ts +73 -0
- package/src/utils/tokens.ts +4 -2
- package/src/__tests__/cardCrop.test.ts +0 -39
- package/src/__tests__/deviceMetadata.test.ts +0 -34
- package/src/__tests__/errors.test.ts +0 -34
- package/src/__tests__/flow.test.ts +0 -61
- package/src/__tests__/gestureDetector.test.ts +0 -37
- package/src/__tests__/liveness.test.ts +0 -112
- package/src/__tests__/resolveUrl.test.ts +0 -64
- package/src/__tests__/validators.test.ts +0 -38
|
@@ -45,6 +45,10 @@ class HybridMyazaFaceDetector : HybridMyazaFaceDetectorSpec() {
|
|
|
45
45
|
// frame → CameraX ImageProxy → the underlying android.media.Image for ML Kit.
|
|
46
46
|
val proxy: ImageProxy = (frame as? NativeFrame)?.image ?: return noFace(128.0)
|
|
47
47
|
val brightness = averageLuma(proxy)
|
|
48
|
+
// Mean RGB of the face region, for flash (screen-reflection) liveness.
|
|
49
|
+
// Taken in this same pass because the planes are already mapped — a second
|
|
50
|
+
// native module would walk the whole frame again for numbers available now.
|
|
51
|
+
val faceRgb = averageFaceRgb(proxy)
|
|
48
52
|
val mediaImage = proxy.image ?: return noFace(brightness)
|
|
49
53
|
|
|
50
54
|
val rotation = proxy.imageInfo.rotationDegrees
|
|
@@ -55,7 +59,9 @@ class HybridMyazaFaceDetector : HybridMyazaFaceDetectorSpec() {
|
|
|
55
59
|
detector.process(input)
|
|
56
60
|
.addOnSuccessListener { faces ->
|
|
57
61
|
val face = faces.maxByOrNull { it.boundingBox.width() * it.boundingBox.height() }
|
|
58
|
-
result = face?.let {
|
|
62
|
+
result = face?.let {
|
|
63
|
+
toResult(it, mediaImage.width, mediaImage.height, rotation, faces.size, brightness, faceRgb)
|
|
64
|
+
}
|
|
59
65
|
latch.countDown()
|
|
60
66
|
}
|
|
61
67
|
.addOnFailureListener { latch.countDown() }
|
|
@@ -71,11 +77,13 @@ class HybridMyazaFaceDetector : HybridMyazaFaceDetectorSpec() {
|
|
|
71
77
|
rotation: Int,
|
|
72
78
|
faceCount: Int,
|
|
73
79
|
brightness: Double,
|
|
80
|
+
faceRgb: Triple<Double, Double, Double>,
|
|
74
81
|
): FaceResult {
|
|
75
82
|
// After rotation the frame is upright; when rotated 90/270 the stored width/
|
|
76
83
|
// height are swapped, so the smaller dimension is the upright "width".
|
|
77
84
|
val frameWidth = if (rotation == 90 || rotation == 270) imageHeight else imageWidth
|
|
78
85
|
val ratio = (face.boundingBox.width().toDouble() / frameWidth).coerceIn(0.0, 1.0)
|
|
86
|
+
val frameHeight = if (rotation == 90 || rotation == 270) imageWidth else imageHeight
|
|
79
87
|
return FaceResult(
|
|
80
88
|
headEulerAngleX = face.headEulerAngleX.toDouble(),
|
|
81
89
|
headEulerAngleY = face.headEulerAngleY.toDouble(),
|
|
@@ -86,9 +94,82 @@ class HybridMyazaFaceDetector : HybridMyazaFaceDetectorSpec() {
|
|
|
86
94
|
faceSizeRatio = ratio,
|
|
87
95
|
faceCount = faceCount.toDouble(),
|
|
88
96
|
brightness = brightness,
|
|
97
|
+
// Face centre, so the TS continuity guard can tell "the same face moved"
|
|
98
|
+
// from "a different face appeared" — size alone cannot, since two people
|
|
99
|
+
// at the same distance measure alike.
|
|
100
|
+
faceCenterX = (face.boundingBox.exactCenterX().toDouble() / frameWidth).coerceIn(0.0, 1.0),
|
|
101
|
+
faceCenterY = (face.boundingBox.exactCenterY().toDouble() / frameHeight).coerceIn(0.0, 1.0),
|
|
102
|
+
// ML Kit's per-face id, stable while it follows the SAME face. A change is
|
|
103
|
+
// PROOF of a different face — something Vision cannot report on iOS — so
|
|
104
|
+
// it strengthens the geometric guard rather than replacing it.
|
|
105
|
+
trackingId = (face.trackingId ?: -1).toDouble(),
|
|
106
|
+
faceR = faceRgb.first,
|
|
107
|
+
faceG = faceRgb.second,
|
|
108
|
+
faceB = faceRgb.third,
|
|
89
109
|
)
|
|
90
110
|
}
|
|
91
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Mean RGB (0–255 each) of the centre crop, where the positioning gate has
|
|
114
|
+
* already put the face. Flash liveness measures how that patch of skin
|
|
115
|
+
* reflects each emitted colour.
|
|
116
|
+
*
|
|
117
|
+
* The crop matches the web and iOS SDKs (centre 40% × 50%) so all three — and
|
|
118
|
+
* the server's re-analysis of the recorded video — measure the same region.
|
|
119
|
+
* Sampled from the raw YUV planes rather than converting the frame: a full
|
|
120
|
+
* colour conversion per frame would cost far more than the ~1k samples taken
|
|
121
|
+
* here.
|
|
122
|
+
*/
|
|
123
|
+
private fun averageFaceRgb(image: ImageProxy): Triple<Double, Double, Double> {
|
|
124
|
+
val yPlane = image.planes.getOrNull(0) ?: return Triple(-1.0, -1.0, -1.0)
|
|
125
|
+
val uPlane = image.planes.getOrNull(1) ?: return Triple(-1.0, -1.0, -1.0)
|
|
126
|
+
val vPlane = image.planes.getOrNull(2) ?: return Triple(-1.0, -1.0, -1.0)
|
|
127
|
+
val w = image.width
|
|
128
|
+
val h = image.height
|
|
129
|
+
if (w <= 0 || h <= 0) return Triple(-1.0, -1.0, -1.0)
|
|
130
|
+
|
|
131
|
+
val x0 = (w * 0.3).toInt()
|
|
132
|
+
val x1 = (w * 0.7).toInt()
|
|
133
|
+
val y0 = (h * 0.25).toInt()
|
|
134
|
+
val y1 = (h * 0.75).toInt()
|
|
135
|
+
val stepX = maxOf(1, (x1 - x0) / 32)
|
|
136
|
+
val stepY = maxOf(1, (y1 - y0) / 32)
|
|
137
|
+
|
|
138
|
+
val yBuf = yPlane.buffer
|
|
139
|
+
val uBuf = uPlane.buffer
|
|
140
|
+
val vBuf = vPlane.buffer
|
|
141
|
+
var rs = 0.0
|
|
142
|
+
var gs = 0.0
|
|
143
|
+
var bs = 0.0
|
|
144
|
+
var count = 0
|
|
145
|
+
|
|
146
|
+
var y = y0
|
|
147
|
+
while (y < y1) {
|
|
148
|
+
var x = x0
|
|
149
|
+
while (x < x1) {
|
|
150
|
+
val yIdx = y * yPlane.rowStride + x * yPlane.pixelStride
|
|
151
|
+
// Chroma is quarter-resolution on 4:2:0, hence the halved coordinates.
|
|
152
|
+
val cIdx = (y / 2) * uPlane.rowStride + (x / 2) * uPlane.pixelStride
|
|
153
|
+
val vIdx = (y / 2) * vPlane.rowStride + (x / 2) * vPlane.pixelStride
|
|
154
|
+
if (yIdx < yBuf.limit() && cIdx < uBuf.limit() && vIdx < vBuf.limit()) {
|
|
155
|
+
val luma = (yBuf.get(yIdx).toInt() and 0xFF).toDouble()
|
|
156
|
+
val cb = (uBuf.get(cIdx).toInt() and 0xFF) - 128.0
|
|
157
|
+
val cr = (vBuf.get(vIdx).toInt() and 0xFF) - 128.0
|
|
158
|
+
rs += luma + 1.402 * cr
|
|
159
|
+
gs += luma - 0.344136 * cb - 0.714136 * cr
|
|
160
|
+
bs += luma + 1.772 * cb
|
|
161
|
+
count++
|
|
162
|
+
}
|
|
163
|
+
x += stepX
|
|
164
|
+
}
|
|
165
|
+
y += stepY
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (count == 0) return Triple(-1.0, -1.0, -1.0)
|
|
169
|
+
fun clamp(v: Double) = (v / count).coerceIn(0.0, 255.0)
|
|
170
|
+
return Triple(clamp(rs), clamp(gs), clamp(bs))
|
|
171
|
+
}
|
|
172
|
+
|
|
92
173
|
/** Mean luma (0–255) sampled on a coarse ~64×48 grid from the Y plane. */
|
|
93
174
|
private fun averageLuma(image: ImageProxy): Double {
|
|
94
175
|
val plane = image.planes.getOrNull(0) ?: return 128.0
|
|
@@ -130,5 +211,11 @@ class HybridMyazaFaceDetector : HybridMyazaFaceDetectorSpec() {
|
|
|
130
211
|
faceSizeRatio = 0.0,
|
|
131
212
|
faceCount = 0.0,
|
|
132
213
|
brightness = brightness,
|
|
214
|
+
faceCenterX = -1.0,
|
|
215
|
+
faceCenterY = -1.0,
|
|
216
|
+
trackingId = -1.0,
|
|
217
|
+
faceR = -1.0,
|
|
218
|
+
faceG = -1.0,
|
|
219
|
+
faceB = -1.0,
|
|
133
220
|
)
|
|
134
221
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
package com.margelo.nitro.myazakyc
|
|
2
|
+
|
|
3
|
+
import com.margelo.nitro.camera.HybridFrameSpec
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Android half of document edge detection — deliberately a NO-OP.
|
|
7
|
+
*
|
|
8
|
+
* ML Kit has no frame-by-frame rectangle detector, and there is no honest way to
|
|
9
|
+
* fake one: a hand-rolled contour finder over camera frames would be slower than
|
|
10
|
+
* the text pass it sits beside and far less reliable, and a detector that
|
|
11
|
+
* reports a box it isn't sure about is worse than none, because auto-capture
|
|
12
|
+
* would shoot on it.
|
|
13
|
+
*
|
|
14
|
+
* So Android's auto-capture rides TEXT recognition alone. That is not a
|
|
15
|
+
* degraded path — the text gate answers framing (from the region the recognised
|
|
16
|
+
* text occupies) and identity (from the printed words) by itself on both
|
|
17
|
+
* platforms; iOS simply gets geometry as an extra corroborating signal.
|
|
18
|
+
*
|
|
19
|
+
* Reporting `found = false` is what makes the shared TypeScript gate fall back
|
|
20
|
+
* cleanly, with no platform branch at the call site.
|
|
21
|
+
*/
|
|
22
|
+
class HybridMyazaRectDetector : HybridMyazaRectDetectorSpec() {
|
|
23
|
+
|
|
24
|
+
override fun detectRect(frame: HybridFrameSpec): DetectedRect =
|
|
25
|
+
DetectedRect(
|
|
26
|
+
found = false,
|
|
27
|
+
x = 0.0,
|
|
28
|
+
y = 0.0,
|
|
29
|
+
width = 0.0,
|
|
30
|
+
height = 0.0,
|
|
31
|
+
aspect = 0.0,
|
|
32
|
+
confidence = 0.0,
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
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 android.net.Uri
|
|
8
|
+
import com.google.mlkit.vision.common.InputImage
|
|
9
|
+
import com.margelo.nitro.NitroModules
|
|
10
|
+
import com.margelo.nitro.core.Promise
|
|
11
|
+
import com.google.mlkit.vision.text.TextRecognition
|
|
12
|
+
import com.google.mlkit.vision.text.TextRecognizer
|
|
13
|
+
import com.google.mlkit.vision.text.latin.TextRecognizerOptions
|
|
14
|
+
import java.util.concurrent.CountDownLatch
|
|
15
|
+
import java.util.concurrent.TimeUnit
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Android half of on-device text recognition, implementing the nitrogen-generated
|
|
19
|
+
* `HybridMyazaTextRecognizerSpec` from src/specs/MyazaTextRecognizer.nitro.ts.
|
|
20
|
+
* The iOS half is HybridMyazaTextRecognizer.swift (Apple Vision).
|
|
21
|
+
*
|
|
22
|
+
* Used for reading the MRZ off a passport or ID card. Returns LINES only —
|
|
23
|
+
* parsing lives in TypeScript, where it is testable against the ICAO specimen
|
|
24
|
+
* without a device, and where the check digits do the actual deciding.
|
|
25
|
+
*
|
|
26
|
+
* Like the face detector, the Nitro worklet calls this synchronously, so ML
|
|
27
|
+
* Kit's async recogniser is awaited on the worklet thread with a short bound.
|
|
28
|
+
* Text recognition is heavier than face detection, hence the longer timeout —
|
|
29
|
+
* but it still returns empty rather than stalling the camera pipeline.
|
|
30
|
+
*/
|
|
31
|
+
class HybridMyazaTextRecognizer : HybridMyazaTextRecognizerSpec() {
|
|
32
|
+
private val recognizer: TextRecognizer =
|
|
33
|
+
TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
|
|
34
|
+
|
|
35
|
+
@ExperimentalGetImage
|
|
36
|
+
override fun recognizeText(frame: HybridFrameSpec, bottomFraction: Double): TextResult {
|
|
37
|
+
val proxy: ImageProxy = (frame as? NativeFrame)?.image ?: return TextResult(emptyArray())
|
|
38
|
+
val mediaImage = proxy.image ?: return TextResult(emptyArray())
|
|
39
|
+
|
|
40
|
+
val rotation = proxy.imageInfo.rotationDegrees
|
|
41
|
+
val input = InputImage.fromMediaImage(mediaImage, rotation)
|
|
42
|
+
|
|
43
|
+
// The upright frame height: when rotated 90/270 the stored dimensions swap.
|
|
44
|
+
val uprightHeight = if (rotation == 90 || rotation == 270) mediaImage.width else mediaImage.height
|
|
45
|
+
val fraction = bottomFraction.coerceIn(0.05, 1.0)
|
|
46
|
+
// ML Kit has no region-of-interest, so the crop is applied to the RESULT:
|
|
47
|
+
// anything whose box sits above the band is page text, not MRZ. That still
|
|
48
|
+
// buys the important half of the benefit — the printed fields stop
|
|
49
|
+
// competing with the MRZ for the extractor's attention.
|
|
50
|
+
val cutoffY = uprightHeight * (1.0 - fraction)
|
|
51
|
+
|
|
52
|
+
var lines: Array<String> = emptyArray()
|
|
53
|
+
val latch = CountDownLatch(1)
|
|
54
|
+
recognizer.process(input)
|
|
55
|
+
.addOnSuccessListener { text ->
|
|
56
|
+
lines = text.textBlocks
|
|
57
|
+
.flatMap { it.lines }
|
|
58
|
+
.filter { line ->
|
|
59
|
+
val box = line.boundingBox
|
|
60
|
+
// A line with no box cannot be placed, so it is kept rather than
|
|
61
|
+
// dropped — the check digits will reject it if it is noise.
|
|
62
|
+
box == null || fraction >= 1.0 || box.centerY() >= cutoffY
|
|
63
|
+
}
|
|
64
|
+
// Top to bottom: the MRZ's rows must arrive adjacent and in order for
|
|
65
|
+
// the extractor's grouping to work.
|
|
66
|
+
.sortedBy { it.boundingBox?.centerY() ?: 0 }
|
|
67
|
+
.map { it.text }
|
|
68
|
+
.toTypedArray()
|
|
69
|
+
latch.countDown()
|
|
70
|
+
}
|
|
71
|
+
.addOnFailureListener { latch.countDown() }
|
|
72
|
+
|
|
73
|
+
// 2s, not the 400ms this shipped with. ML Kit runs full-frame latin
|
|
74
|
+
// recognition here (it has no ROI), which routinely takes longer than
|
|
75
|
+
// 400ms on mid-tier hardware — so the await expired and returned EMPTY,
|
|
76
|
+
// and auto-capture on Android almost never saw a single line of text.
|
|
77
|
+
// The frame processor drops frames while busy, so a longer wait costs
|
|
78
|
+
// fps, not a queue; an empty result costs the whole feature.
|
|
79
|
+
latch.await(2000, TimeUnit.MILLISECONDS)
|
|
80
|
+
return TextResult(lines)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
override fun recognizeTextInImage(uri: String, bottomFraction: Double): Promise<TextResult> {
|
|
84
|
+
return Promise.async {
|
|
85
|
+
val ctx = NitroModules.applicationContext
|
|
86
|
+
?: return@async TextResult(emptyArray())
|
|
87
|
+
// A still is already upright — unlike the live frame, which arrives
|
|
88
|
+
// rotated by the camera pipeline and needs the rotation passed in.
|
|
89
|
+
val input = runCatching { InputImage.fromFilePath(ctx, Uri.parse(uri)) }
|
|
90
|
+
.getOrNull() ?: return@async TextResult(emptyArray())
|
|
91
|
+
|
|
92
|
+
val fraction = bottomFraction.coerceIn(0.05, 1.0)
|
|
93
|
+
val cutoffY = input.height * (1.0 - fraction)
|
|
94
|
+
|
|
95
|
+
val latch = CountDownLatch(1)
|
|
96
|
+
var lines: Array<String> = emptyArray()
|
|
97
|
+
recognizer.process(input)
|
|
98
|
+
.addOnSuccessListener { text ->
|
|
99
|
+
lines = text.textBlocks
|
|
100
|
+
.flatMap { it.lines }
|
|
101
|
+
.filter { line ->
|
|
102
|
+
val box = line.boundingBox
|
|
103
|
+
box == null || fraction >= 1.0 || box.centerY() >= cutoffY
|
|
104
|
+
}
|
|
105
|
+
.sortedBy { it.boundingBox?.centerY() ?: 0 }
|
|
106
|
+
.map { it.text }
|
|
107
|
+
.toTypedArray()
|
|
108
|
+
latch.countDown()
|
|
109
|
+
}
|
|
110
|
+
.addOnFailureListener { latch.countDown() }
|
|
111
|
+
|
|
112
|
+
// A still can be large, so this is more generous than the per-frame path
|
|
113
|
+
// — it runs once, off the camera thread, and the user is already waiting.
|
|
114
|
+
latch.await(4, TimeUnit.SECONDS)
|
|
115
|
+
TextResult(lines)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
package com.margelo.nitro.myazakyc
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import jj2000.j2k.codestream.HeaderInfo
|
|
5
|
+
import jj2000.j2k.codestream.reader.BitstreamReaderAgent
|
|
6
|
+
import jj2000.j2k.codestream.reader.HeaderDecoder
|
|
7
|
+
import jj2000.j2k.decoder.Decoder
|
|
8
|
+
import jj2000.j2k.fileformat.reader.FileFormatReader
|
|
9
|
+
import jj2000.j2k.image.DataBlkInt
|
|
10
|
+
import jj2000.j2k.image.ImgDataConverter
|
|
11
|
+
import jj2000.j2k.image.invcomptransf.InvCompTransf
|
|
12
|
+
import jj2000.j2k.util.ISRandomAccessIO
|
|
13
|
+
import jj2000.j2k.util.ParameterList
|
|
14
|
+
import jj2000.j2k.wavelet.synthesis.InverseWT
|
|
15
|
+
import java.io.ByteArrayInputStream
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* JPEG 2000 → Bitmap, for the DG2 chip portrait.
|
|
19
|
+
*
|
|
20
|
+
* Android's BitmapFactory has NO JPEG 2000 decoder — on iOS ImageIO registers
|
|
21
|
+
* `public.jpeg-2000`, which is why the portrait always showed there and never
|
|
22
|
+
* here (the Flutter SDK documents the same asymmetry in dg2_image.dart). Since
|
|
23
|
+
* no platform API will ever decode it, the decoder is bundled: JJ2000, the
|
|
24
|
+
* JPEG 2000 reference implementation (pure Java, `edu.ucar:jj2000`), driven
|
|
25
|
+
* through the same header→bitstream→entropy→dequant→inverse-DWT→inverse-CT
|
|
26
|
+
* chain JMRTD's Android decoder uses.
|
|
27
|
+
*
|
|
28
|
+
* Null on ANY failure, including exotic codestreams (component subsampling) —
|
|
29
|
+
* the portrait is a courtesy and the caller falls back to the generic mark,
|
|
30
|
+
* exactly as it does today.
|
|
31
|
+
*/
|
|
32
|
+
internal object Jp2Decoder {
|
|
33
|
+
|
|
34
|
+
fun decode(bytes: ByteArray): Bitmap? =
|
|
35
|
+
try {
|
|
36
|
+
decodeUnsafe(bytes)
|
|
37
|
+
} catch (_: Throwable) {
|
|
38
|
+
null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private fun decodeUnsafe(bytes: ByteArray): Bitmap? {
|
|
42
|
+
val input = ISRandomAccessIO(ByteArrayInputStream(bytes), bytes.size, 1, bytes.size)
|
|
43
|
+
|
|
44
|
+
// The decoder's own defaults; JJ2000 refuses to run without a full list.
|
|
45
|
+
val defaults = ParameterList()
|
|
46
|
+
for (p in Decoder.getAllParameters()) if (p[3] != null) defaults[p[0]] = p[3]
|
|
47
|
+
val pl = ParameterList(defaults)
|
|
48
|
+
|
|
49
|
+
// DG2 stores either a bare codestream or the full JP2 container; the
|
|
50
|
+
// FileFormatReader tells them apart and finds the codestream box.
|
|
51
|
+
val ff = FileFormatReader(input)
|
|
52
|
+
ff.readFileFormat()
|
|
53
|
+
if (ff.JP2FFUsed) input.seek(ff.firstCodeStreamPos)
|
|
54
|
+
|
|
55
|
+
val hi = HeaderInfo()
|
|
56
|
+
val hd = HeaderDecoder(input, pl, hi)
|
|
57
|
+
val nComp = hd.numComps
|
|
58
|
+
if (nComp != 1 && nComp != 3) return null
|
|
59
|
+
val depth = IntArray(nComp) { hd.getOriginalBitDepth(it) }
|
|
60
|
+
val decSpec = hd.decoderSpecs
|
|
61
|
+
|
|
62
|
+
val reader = BitstreamReaderAgent.createInstance(input, hd, pl, decSpec, false, hi)
|
|
63
|
+
val entropy = hd.createEntropyDecoder(reader, pl)
|
|
64
|
+
val roi = hd.createROIDeScaler(entropy, pl, decSpec)
|
|
65
|
+
val dequant = hd.createDequantizer(roi, depth, decSpec)
|
|
66
|
+
val invWT = InverseWT.createInstance(dequant, decSpec)
|
|
67
|
+
invWT.setImgResLevel(reader.imgRes)
|
|
68
|
+
val converter = ImgDataConverter(invWT, 0)
|
|
69
|
+
val ict = InvCompTransf(converter, decSpec, depth, pl)
|
|
70
|
+
|
|
71
|
+
ict.setTile(0, 0)
|
|
72
|
+
val width = ict.imgWidth
|
|
73
|
+
val height = ict.imgHeight
|
|
74
|
+
if (width <= 0 || height <= 0 || width * height > 4_000_000) return null
|
|
75
|
+
// Subsampled components would need per-component upsampling; every real
|
|
76
|
+
// DG2 portrait observed is 4:4:4 or grayscale, so anything else bails to
|
|
77
|
+
// the caller's fallback rather than risking a garbled face.
|
|
78
|
+
for (c in 0 until nComp) {
|
|
79
|
+
if (ict.getCompImgWidth(c) != width || ict.getCompImgHeight(c) != height) return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
val channels = Array(nComp) { c ->
|
|
83
|
+
val blk = DataBlkInt(0, 0, width, height)
|
|
84
|
+
var out = ict.getInternCompData(blk, c) as DataBlkInt
|
|
85
|
+
// A progressive return means the data isn't final yet — ask again.
|
|
86
|
+
while (out.progressive) out = ict.getInternCompData(out, c) as DataBlkInt
|
|
87
|
+
|
|
88
|
+
val fixed = ict.getFixedPoint(c)
|
|
89
|
+
val shift = 1 shl (depth[c] - 1)
|
|
90
|
+
val max = (1 shl depth[c]) - 1
|
|
91
|
+
val plane = IntArray(width * height)
|
|
92
|
+
for (y in 0 until height) {
|
|
93
|
+
var src = out.offset + y * out.scanw
|
|
94
|
+
var dst = y * width
|
|
95
|
+
for (x in 0 until width) {
|
|
96
|
+
// Undo the fixed-point scaling, then the DC level shift, clamp to
|
|
97
|
+
// the component's range, and normalise to 8 bits.
|
|
98
|
+
var v = (out.data[src] shr fixed) + shift
|
|
99
|
+
if (v < 0) v = 0 else if (v > max) v = max
|
|
100
|
+
plane[dst] = if (max == 255) v else (v * 255) / max
|
|
101
|
+
src += 1
|
|
102
|
+
dst += 1
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
plane
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
val argb = IntArray(width * height)
|
|
109
|
+
val r = channels[0]
|
|
110
|
+
val g = if (nComp == 3) channels[1] else channels[0]
|
|
111
|
+
val b = if (nComp == 3) channels[2] else channels[0]
|
|
112
|
+
for (i in argb.indices) {
|
|
113
|
+
argb[i] = (0xFF shl 24) or (r[i] shl 16) or (g[i] shl 8) or b[i]
|
|
114
|
+
}
|
|
115
|
+
return Bitmap.createBitmap(argb, width, height, Bitmap.Config.ARGB_8888)
|
|
116
|
+
}
|
|
117
|
+
}
|
package/app.plugin.js
CHANGED
|
@@ -1,28 +1,58 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
withInfoPlist,
|
|
3
|
+
withEntitlementsPlist,
|
|
4
|
+
withAndroidManifest,
|
|
5
|
+
createRunOncePlugin,
|
|
6
|
+
AndroidConfig,
|
|
7
|
+
} = require('expo/config-plugins');
|
|
2
8
|
|
|
3
9
|
const pkg = require('./package.json');
|
|
4
10
|
|
|
5
11
|
// ---------------------------------------------------------------------------
|
|
6
12
|
// Expo config plugin for @myazahq/kyc-sdk-react-native.
|
|
7
13
|
//
|
|
8
|
-
// The native face
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// the things that must be written into the HOST app's native config:
|
|
14
|
+
// The native modules (face detector, text recogniser, eMRTD reader) autolink
|
|
15
|
+
// via expo-module.config.json. This plugin only handles what must be written
|
|
16
|
+
// into the HOST app's native config:
|
|
12
17
|
//
|
|
13
|
-
// • iOS — NSCameraUsageDescription (document scan + selfie liveness)
|
|
18
|
+
// • iOS — NSCameraUsageDescription (document scan + selfie liveness), and,
|
|
19
|
+
// when NFC is enabled, NFCReaderUsageDescription plus the
|
|
20
|
+
// reader-session entitlements.
|
|
14
21
|
// NOTE: voice guidance is TTS OUTPUT only, so NO microphone usage
|
|
15
22
|
// string and no NSMicrophoneUsageDescription are added.
|
|
16
|
-
// • Android — CAMERA + INTERNET
|
|
23
|
+
// • Android — CAMERA + INTERNET, plus NFC when enabled.
|
|
17
24
|
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
25
|
+
// NFC is OPT-IN (`nfc: true`). The entitlement requires the App ID to carry the
|
|
26
|
+
// "NFC Tag Reading" capability in the Apple Developer portal; adding it
|
|
27
|
+
// unconditionally would break code-signing for every consumer who does not read
|
|
28
|
+
// chips and has not enabled it.
|
|
29
|
+
//
|
|
30
|
+
// Camera preview/capture itself comes from react-native-vision-camera; add its
|
|
31
|
+
// config plugin too. This one is intentionally minimal and idempotent.
|
|
21
32
|
// ---------------------------------------------------------------------------
|
|
22
33
|
|
|
23
34
|
const CAMERA_USAGE =
|
|
24
35
|
'We use the camera to photograph your ID document and to capture a live selfie for identity verification.';
|
|
25
36
|
|
|
37
|
+
const NFC_USAGE =
|
|
38
|
+
'We read the secure chip in your passport or ID card to confirm the document is genuine.';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The eMRTD application identifier.
|
|
42
|
+
*
|
|
43
|
+
* iOS will not open an ISO-7816 session at all unless the AID the app intends
|
|
44
|
+
* to select is declared — an undeclared one fails at connect time with an error
|
|
45
|
+
* that says nothing about the cause.
|
|
46
|
+
*/
|
|
47
|
+
const EMRTD_AID = 'A0000002471001';
|
|
48
|
+
|
|
49
|
+
/** Adds a value to a plist array, leaving any existing entries alone. */
|
|
50
|
+
function addToList(container, key, value) {
|
|
51
|
+
const current = container[key];
|
|
52
|
+
const list = Array.isArray(current) ? current : [];
|
|
53
|
+
if (!list.includes(value)) container[key] = [...list, value];
|
|
54
|
+
}
|
|
55
|
+
|
|
26
56
|
function withIosCameraUsage(config, customMessage) {
|
|
27
57
|
return withInfoPlist(config, (cfg) => {
|
|
28
58
|
if (!cfg.modResults.NSCameraUsageDescription) {
|
|
@@ -32,11 +62,62 @@ function withIosCameraUsage(config, customMessage) {
|
|
|
32
62
|
});
|
|
33
63
|
}
|
|
34
64
|
|
|
35
|
-
|
|
65
|
+
const AID_KEY = 'com.apple.developer.nfc.readersession.iso7816.select-identifiers';
|
|
66
|
+
const FORMATS_KEY = 'com.apple.developer.nfc.readersession.formats';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Info.plist half of NFC. Exported so the split below can be tested without a
|
|
70
|
+
* device build — which is the only thing that caught it being wrong.
|
|
71
|
+
* @param {Record<string, unknown>} plist
|
|
72
|
+
*/
|
|
73
|
+
function applyNfcInfoPlist(plist, customMessage) {
|
|
74
|
+
if (!plist.NFCReaderUsageDescription) {
|
|
75
|
+
plist.NFCReaderUsageDescription = customMessage || NFC_USAGE;
|
|
76
|
+
}
|
|
77
|
+
// The AID list is an Info.plist key, NOT an entitlement — Apple documents it
|
|
78
|
+
// there, and the App ID's "NFC Tag Reading" capability covers only the
|
|
79
|
+
// formats entitlement. Adding it to the entitlements file as well fails the
|
|
80
|
+
// build outright: Xcode finds no matching capability to put in the profile
|
|
81
|
+
// and refuses to sign ("not found and could not be included in profile").
|
|
82
|
+
addToList(plist, AID_KEY, EMRTD_AID);
|
|
83
|
+
return plist;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Entitlements half of NFC.
|
|
88
|
+
* @param {Record<string, unknown>} plist
|
|
89
|
+
*/
|
|
90
|
+
function applyNfcEntitlements(plist) {
|
|
91
|
+
// 'TAG' is the raw ISO-7816 format a passport needs; 'NDEF' is a different
|
|
92
|
+
// capability entirely and does not cover it.
|
|
93
|
+
addToList(plist, FORMATS_KEY, 'TAG');
|
|
94
|
+
// Actively REMOVE the AID list from here. An earlier version of this plugin
|
|
95
|
+
// wrote it to the entitlements too, and prebuild MERGES into whatever
|
|
96
|
+
// entitlements file already exists — so without this, anyone who built once
|
|
97
|
+
// with that version keeps a key that makes signing fail forever, and
|
|
98
|
+
// re-running prebuild never clears it.
|
|
99
|
+
delete plist[AID_KEY];
|
|
100
|
+
return plist;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function withIosNfc(config, customMessage) {
|
|
104
|
+
config = withInfoPlist(config, (cfg) => {
|
|
105
|
+
applyNfcInfoPlist(cfg.modResults, customMessage);
|
|
106
|
+
return cfg;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return withEntitlementsPlist(config, (cfg) => {
|
|
110
|
+
applyNfcEntitlements(cfg.modResults);
|
|
111
|
+
return cfg;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function withAndroidPermissions(config, { nfc }) {
|
|
36
116
|
return withAndroidManifest(config, (cfg) => {
|
|
37
117
|
AndroidConfig.Permissions.ensurePermissions(cfg.modResults, [
|
|
38
118
|
'android.permission.CAMERA',
|
|
39
119
|
'android.permission.INTERNET',
|
|
120
|
+
...(nfc ? ['android.permission.NFC'] : []),
|
|
40
121
|
]);
|
|
41
122
|
return cfg;
|
|
42
123
|
});
|
|
@@ -44,12 +125,20 @@ function withAndroidCameraPermissions(config) {
|
|
|
44
125
|
|
|
45
126
|
/**
|
|
46
127
|
* @param {object} config
|
|
47
|
-
* @param {{ cameraPermission?: string }} [props]
|
|
128
|
+
* @param {{ cameraPermission?: string, nfc?: boolean, nfcPermission?: string }} [props]
|
|
48
129
|
*/
|
|
49
130
|
function withMyazaKyc(config, props = {}) {
|
|
50
131
|
config = withIosCameraUsage(config, props.cameraPermission);
|
|
51
|
-
config =
|
|
132
|
+
config = withAndroidPermissions(config, { nfc: props.nfc === true });
|
|
133
|
+
if (props.nfc === true) {
|
|
134
|
+
config = withIosNfc(config, props.nfcPermission);
|
|
135
|
+
}
|
|
52
136
|
return config;
|
|
53
137
|
}
|
|
54
138
|
|
|
55
139
|
module.exports = createRunOncePlugin(withMyazaKyc, pkg.name, pkg.version);
|
|
140
|
+
// Named exports for the tests. The plugin itself stays the default export, so
|
|
141
|
+
// consumers' app.json entries are unaffected.
|
|
142
|
+
module.exports.applyNfcInfoPlist = applyNfcInfoPlist;
|
|
143
|
+
module.exports.applyNfcEntitlements = applyNfcEntitlements;
|
|
144
|
+
module.exports.EMRTD_AID = EMRTD_AID;
|