@myazahq/kyc-sdk-react-native 2.0.2 → 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/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 +96 -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 +156 -132
- package/src/screens/IdInputStep.tsx +13 -5
- package/src/screens/IdTypeStep.tsx +74 -33
- package/src/screens/KeyPeopleInviteLinks.tsx +239 -0
- package/src/screens/LivenessStep.tsx +155 -247
- 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 +2 -2
- 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 -124
- 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
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LivenessChallenge,
|
|
3
|
+
LivenessConfig,
|
|
4
|
+
LivenessFaceData,
|
|
5
|
+
LivenessFailureReason,
|
|
6
|
+
LivenessPhase,
|
|
7
|
+
} from './types';
|
|
8
|
+
import type { FlashResult } from './flashDetector';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// The liveness machine's vocabulary: its UI state, its options, its guidance
|
|
12
|
+
// strings and the two phase predicates the handlers branch on.
|
|
13
|
+
//
|
|
14
|
+
// Split out of useLiveness (200-line rule). Deliberately free of the hook, so
|
|
15
|
+
// the phase predicates below can be read — and reasoned about — without the
|
|
16
|
+
// state machine around them. They are load-bearing: `isSettled` decides where
|
|
17
|
+
// guidance is suppressed, and getting its scope wrong is what once left the
|
|
18
|
+
// flash and the capture without a multiple-face check.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export const INTEGRITY_FAILED_PATCH = {
|
|
22
|
+
phase: 'failed' as const,
|
|
23
|
+
failureReason: 'face_swap' as const,
|
|
24
|
+
instruction: 'Let\u2019s start over \u2014 please stay in frame.',
|
|
25
|
+
activeChallenge: null,
|
|
26
|
+
positionGuidance: null,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
import { LivenessSpeaker } from './speech';
|
|
30
|
+
import type { VoiceGuidanceOption } from '../types/config';
|
|
31
|
+
|
|
32
|
+
export const CHALLENGE_PASSED_MS = 700; // brief "Great!" before the next challenge
|
|
33
|
+
export const POSITION_TOO_FAR = 'too_far';
|
|
34
|
+
export const POSITION_TOO_CLOSE = 'too_close';
|
|
35
|
+
export const MULTIPLE_FACES_GUIDANCE = 'Make sure only your face is visible';
|
|
36
|
+
|
|
37
|
+
export type PositionGuidance = typeof POSITION_TOO_FAR | typeof POSITION_TOO_CLOSE;
|
|
38
|
+
export type LightingGuidance = 'dark' | 'bright';
|
|
39
|
+
|
|
40
|
+
export interface LivenessUiState {
|
|
41
|
+
phase: LivenessPhase;
|
|
42
|
+
instruction: string;
|
|
43
|
+
activeChallenge: LivenessChallenge | null;
|
|
44
|
+
timeoutRemaining: number;
|
|
45
|
+
completedCount: number;
|
|
46
|
+
totalCount: number;
|
|
47
|
+
positionGuidance: PositionGuidance | null;
|
|
48
|
+
lightingGuidance: LightingGuidance | null;
|
|
49
|
+
multipleFaces: boolean;
|
|
50
|
+
wrongGesture: boolean;
|
|
51
|
+
faceDetected: boolean;
|
|
52
|
+
failureReason: LivenessFailureReason | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface UseLivenessOptions {
|
|
56
|
+
config?: Partial<LivenessConfig>;
|
|
57
|
+
voiceGuidance?: VoiceGuidanceOption;
|
|
58
|
+
/** Fires once when all challenges pass and the phase enters `capturing`. */
|
|
59
|
+
onReadyToCapture?: () => void;
|
|
60
|
+
/**
|
|
61
|
+
* Whether the step is actually on screen and the user has started.
|
|
62
|
+
*
|
|
63
|
+
* The machine is constructed while the readiness primer is still up — hooks
|
|
64
|
+
* cannot be called conditionally — so without this it announces "Position
|
|
65
|
+
* your face in the circle" to someone still reading the primer, before they
|
|
66
|
+
* have tapped anything. Guidance for a screen the user cannot see is worse
|
|
67
|
+
* than none: it is a voice from nowhere.
|
|
68
|
+
*
|
|
69
|
+
* Defaults true so a consumer that never sets it behaves as before.
|
|
70
|
+
*/
|
|
71
|
+
announce?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface UseLivenessReturn extends LivenessUiState {
|
|
75
|
+
/** Push a face frame into the machine (call from the frame processor via runOnJS). */
|
|
76
|
+
onFace: (data: LivenessFaceData) => void;
|
|
77
|
+
/** Push a "no face this frame" signal. */
|
|
78
|
+
onNoFace: () => void;
|
|
79
|
+
/**
|
|
80
|
+
* Resolves true once a face is detected AFTER the call, or false on timeout.
|
|
81
|
+
* The capture gate uses this so an empty frame is never photographed.
|
|
82
|
+
*/
|
|
83
|
+
awaitFreshFace: (timeoutMs: number) => Promise<boolean>;
|
|
84
|
+
/** Feed live lighting quality (null = ok). */
|
|
85
|
+
setLighting: (guidance: LightingGuidance | null) => void;
|
|
86
|
+
/** Mark the selfie captured → phase `complete`. */
|
|
87
|
+
markComplete: () => void;
|
|
88
|
+
/** Restart the session with a fresh random challenge set. */
|
|
89
|
+
reset: () => void;
|
|
90
|
+
/** True while the machine wants a still captured (phase === 'capturing'). */
|
|
91
|
+
shouldCapture: boolean;
|
|
92
|
+
/** True while the screen should be running the flash sequence. */
|
|
93
|
+
shouldFlash: boolean;
|
|
94
|
+
/** Called by the screen when the flash sequence finishes. */
|
|
95
|
+
completeFlash: (result: FlashResult) => void;
|
|
96
|
+
/** The flash verdict, once run — carried into the submission as context. */
|
|
97
|
+
flashResult: FlashResult | null;
|
|
98
|
+
/** How many face discontinuities the continuity guard saw this session. */
|
|
99
|
+
faceGlitches: number;
|
|
100
|
+
/**
|
|
101
|
+
* True once the session can no longer vouch for who is in frame. The capture
|
|
102
|
+
* path reads this AFTER the still is taken: a substitution detected during
|
|
103
|
+
* `capturing` is recorded silently (so nothing re-lays out mid-capture) and
|
|
104
|
+
* acted on here.
|
|
105
|
+
*/
|
|
106
|
+
integrityBroken: boolean;
|
|
107
|
+
/** Live read of the integrity flag — see `isCompromised` in the hook. */
|
|
108
|
+
isCompromised: () => boolean;
|
|
109
|
+
/** Surfaces a failure that was detected mid-capture and deliberately held. */
|
|
110
|
+
reportIntegrityFailure: () => void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Position guidance text (spoken + shown), matching the Flutter SDK exactly. */
|
|
114
|
+
export function positionGuidanceText(g: PositionGuidance): string {
|
|
115
|
+
return g === POSITION_TOO_FAR ? 'Kindly move closer' : 'Kindly move further away';
|
|
116
|
+
}
|
|
117
|
+
/** Lighting warning banner text — the full Flutter strings (shown, not spoken). */
|
|
118
|
+
export function lightingGuidanceText(g: LightingGuidance): string {
|
|
119
|
+
return g === 'dark'
|
|
120
|
+
? 'It looks dark here. Move to a brighter area or near a light source for better detection.'
|
|
121
|
+
: 'Too bright — reduce glare or move away from direct light for better detection.';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Phases during which the screen must NOT change layout.
|
|
126
|
+
*
|
|
127
|
+
* While capturing, a re-layout moves the subject mid-shutter. While flashing,
|
|
128
|
+
* the overlay's cutout is measured once when the flash starts, so shifting the
|
|
129
|
+
* preview underneath it leaves the hole in the wrong place — the exact bug the
|
|
130
|
+
* Flutter SDK hit. Guidance banners are also meaningless during a flash, since
|
|
131
|
+
* the screen is deliberately painting the "wrong" light.
|
|
132
|
+
*/
|
|
133
|
+
export function isSettled(phase: LivenessPhase): boolean {
|
|
134
|
+
return phase === 'capturing' || phase === 'flash';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function isTerminal(phase: LivenessPhase): boolean {
|
|
138
|
+
return phase === 'complete' || phase === 'failed';
|
|
139
|
+
}
|
package/src/liveness/speech.ts
CHANGED
|
@@ -30,11 +30,23 @@ export class LivenessSpeaker {
|
|
|
30
30
|
private readonly enabled: boolean;
|
|
31
31
|
private readonly language: string;
|
|
32
32
|
private lastSpoken: string | null = null;
|
|
33
|
+
private spokeBefore = false;
|
|
33
34
|
|
|
34
35
|
constructor(option: VoiceGuidanceOption | undefined) {
|
|
35
36
|
const resolved = resolveVoiceGuidance(option);
|
|
36
37
|
this.enabled = resolved.enabled;
|
|
37
38
|
this.language = resolved.language;
|
|
39
|
+
if (this.enabled) {
|
|
40
|
+
// Warm the engine NOW, not at the first phrase. Android's TextToSpeech
|
|
41
|
+
// initialises asynchronously, and when the user is well-positioned from
|
|
42
|
+
// the start the first thing this session ever says is the first GESTURE
|
|
43
|
+
// COMMAND — which then races engine init and loses: exactly the phrase
|
|
44
|
+
// a user holding the phone at arm's length cannot afford to miss. Any
|
|
45
|
+
// expo-speech call triggers init; voices is the harmless one. By the
|
|
46
|
+
// time positioning settles the engine is ready and the command plays
|
|
47
|
+
// immediately. iOS initialises synchronously and ignores the warm-up.
|
|
48
|
+
void Speech.getAvailableVoicesAsync().catch(() => undefined);
|
|
49
|
+
}
|
|
38
50
|
}
|
|
39
51
|
|
|
40
52
|
/** Speaks `text` unless guidance is off or it equals the last phrase spoken. */
|
|
@@ -42,7 +54,13 @@ export class LivenessSpeaker {
|
|
|
42
54
|
if (!this.enabled || !text || text === this.lastSpoken) return;
|
|
43
55
|
this.lastSpoken = text;
|
|
44
56
|
try {
|
|
45
|
-
|
|
57
|
+
// stop() exists to cut off a STALE phrase so guidance stays current.
|
|
58
|
+
// Before anything has been spoken there is nothing to cut off — and on
|
|
59
|
+
// Android calling stop() against a still-initialising engine is exactly
|
|
60
|
+
// the poke that can eat the first queued utterance. Skip it until a
|
|
61
|
+
// phrase has actually been issued.
|
|
62
|
+
if (this.spokeBefore) Speech.stop();
|
|
63
|
+
this.spokeBefore = true;
|
|
46
64
|
Speech.speak(text, { language: this.language });
|
|
47
65
|
} catch {
|
|
48
66
|
/* TTS is best-effort — a failure must never block the flow */
|
package/src/liveness/types.ts
CHANGED
|
@@ -32,11 +32,25 @@ export type LivenessPhase =
|
|
|
32
32
|
| 'positioning' // "Position your face in the circle"
|
|
33
33
|
| 'challenge' // Active gesture challenge
|
|
34
34
|
| 'challenge_passed' // Brief green flash
|
|
35
|
+
// Screen-reflection liveness: the display emits a randomised colour sequence
|
|
36
|
+
// and the camera watches the face reflect it. Runs as the FINAL challenge, so
|
|
37
|
+
// by the time it starts the face is already positioned and steady.
|
|
38
|
+
| 'flash'
|
|
35
39
|
| 'capturing' // Auto-capturing selfie
|
|
36
40
|
| 'complete' // All done — selfie review
|
|
37
41
|
| 'failed'; // Timeout or face lost
|
|
38
42
|
|
|
39
|
-
export type LivenessFailureReason =
|
|
43
|
+
export type LivenessFailureReason =
|
|
44
|
+
| 'timeout'
|
|
45
|
+
| 'face_lost'
|
|
46
|
+
| 'no_camera'
|
|
47
|
+
| 'load_error'
|
|
48
|
+
/**
|
|
49
|
+
* A different face appeared mid-session. Liveness proves a live human
|
|
50
|
+
* performed the challenges; only continuity ties that human to the one being
|
|
51
|
+
* captured. Matches the web SDK's `face_swap`.
|
|
52
|
+
*/
|
|
53
|
+
| 'face_swap';
|
|
40
54
|
|
|
41
55
|
// ---------------------------------------------------------------------------
|
|
42
56
|
// Configuration
|
|
@@ -47,12 +61,24 @@ export interface LivenessConfig {
|
|
|
47
61
|
challengePool?: LivenessChallenge[];
|
|
48
62
|
timeoutPerChallenge: number;
|
|
49
63
|
positioningTimeout: number;
|
|
64
|
+
/**
|
|
65
|
+
* Which liveness method runs. 'gestures' (default) is the randomised head
|
|
66
|
+
* movements; 'flash' is screen reflection; 'both' runs gestures then flash,
|
|
67
|
+
* because each catches what the other misses — gestures defeat a static
|
|
68
|
+
* photo, flash defeats a replayed video.
|
|
69
|
+
*/
|
|
70
|
+
mode?: LivenessMode;
|
|
71
|
+
/** Colours in the flash sequence (2–5, default 4). Flash modes only. */
|
|
72
|
+
flashSequenceLength?: number;
|
|
50
73
|
}
|
|
51
74
|
|
|
75
|
+
export type LivenessMode = 'gestures' | 'flash' | 'both';
|
|
76
|
+
|
|
52
77
|
export const DEFAULT_LIVENESS_CONFIG: LivenessConfig = {
|
|
53
78
|
challengeCount: 2,
|
|
54
79
|
timeoutPerChallenge: 8,
|
|
55
80
|
positioningTimeout: 15,
|
|
81
|
+
mode: 'gestures',
|
|
56
82
|
};
|
|
57
83
|
|
|
58
84
|
// ---------------------------------------------------------------------------
|
|
@@ -84,6 +110,27 @@ export interface LivenessFaceData {
|
|
|
84
110
|
faceCount: number;
|
|
85
111
|
/** Mean frame luma (0–255). Feeds the low-light gate (<62 dark, >200 bright). */
|
|
86
112
|
brightness: number;
|
|
113
|
+
/**
|
|
114
|
+
* Face centre in normalised frame coordinates (0–1), when the detector
|
|
115
|
+
* reports it. Together with {@link faceSizeRatio} this is what distinguishes
|
|
116
|
+
* "the same face moved" from "a different face appeared" — size alone cannot,
|
|
117
|
+
* since two people at the same distance measure alike.
|
|
118
|
+
*/
|
|
119
|
+
faceCenterX?: number;
|
|
120
|
+
faceCenterY?: number;
|
|
121
|
+
/**
|
|
122
|
+
* ML Kit's per-face tracking id (Android only; absent on iOS, whose Vision
|
|
123
|
+
* framework has no cross-frame equivalent). When present a CHANGE is proof of
|
|
124
|
+
* a different face — which is why it supplements the geometric continuity
|
|
125
|
+
* guard rather than being relied on alone.
|
|
126
|
+
*/
|
|
127
|
+
trackingId?: number;
|
|
128
|
+
/**
|
|
129
|
+
* Mean RGB of the face region (0–255 each), for flash liveness. Absent when
|
|
130
|
+
* the platform could not sample it — flash then degrades to inconclusive,
|
|
131
|
+
* which fails soft, rather than to a wrong verdict.
|
|
132
|
+
*/
|
|
133
|
+
faceRgb?: readonly [number, number, number];
|
|
87
134
|
}
|
|
88
135
|
|
|
89
136
|
/** Convenience: average of both eye-open probabilities. */
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useCallback, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { FlashResult } from './flashDetector';
|
|
4
|
+
import type { LivenessMode } from './types';
|
|
5
|
+
import type { LivenessUiState } from './useLiveness';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// The flash phase's own state.
|
|
9
|
+
//
|
|
10
|
+
// Split out of useLiveness (200-line rule). Everything here is a REF rather
|
|
11
|
+
// than state on purpose: the pending check is read from inside the challenge
|
|
12
|
+
// transition, and a stale closure there would either skip the flash entirely or
|
|
13
|
+
// run it twice.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export interface FlashPhase {
|
|
17
|
+
/** Whether the flash sequence still has to run. */
|
|
18
|
+
pending: () => boolean;
|
|
19
|
+
/** Called by the screen once the sequence finishes. */
|
|
20
|
+
complete: (result: FlashResult) => void;
|
|
21
|
+
/** The verdict, once run. */
|
|
22
|
+
result: () => FlashResult | null;
|
|
23
|
+
/** Face discontinuities seen this session. */
|
|
24
|
+
glitches: () => number;
|
|
25
|
+
recordGlitch: () => void;
|
|
26
|
+
/** Clears everything for a retry — including the colour sequence. */
|
|
27
|
+
reset: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useFlashPhase(
|
|
31
|
+
mode: LivenessMode | undefined,
|
|
32
|
+
setState: (updater: (s: LivenessUiState) => LivenessUiState) => void,
|
|
33
|
+
): FlashPhase {
|
|
34
|
+
const doneRef = useRef(false);
|
|
35
|
+
const resultRef = useRef<FlashResult | null>(null);
|
|
36
|
+
const glitchesRef = useRef(0);
|
|
37
|
+
|
|
38
|
+
const pending = useCallback(
|
|
39
|
+
() => (mode === 'flash' || mode === 'both') && !doneRef.current,
|
|
40
|
+
[mode],
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The result is carried into the submission as context, NOT used to fail the
|
|
45
|
+
* step: an inconclusive flash (sunlight, or a platform that cannot sample
|
|
46
|
+
* RGB) must not block a user, and the server re-analyses the recorded video
|
|
47
|
+
* regardless. A definite mismatch is what the server acts on.
|
|
48
|
+
*/
|
|
49
|
+
const complete = useCallback(
|
|
50
|
+
(result: FlashResult) => {
|
|
51
|
+
doneRef.current = true;
|
|
52
|
+
resultRef.current = result;
|
|
53
|
+
setState((s) =>
|
|
54
|
+
s.phase === 'flash' ? { ...s, phase: 'capturing', instruction: 'Hold still…' } : s,
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
[setState],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const reset = useCallback(() => {
|
|
61
|
+
// A retry runs the whole check again, INCLUDING a fresh colour sequence —
|
|
62
|
+
// reusing the previous one would hand an attacker the answer.
|
|
63
|
+
doneRef.current = false;
|
|
64
|
+
resultRef.current = null;
|
|
65
|
+
glitchesRef.current = 0;
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
pending,
|
|
70
|
+
complete,
|
|
71
|
+
result: () => resultRef.current,
|
|
72
|
+
glitches: () => glitchesRef.current,
|
|
73
|
+
recordGlitch: () => {
|
|
74
|
+
glitchesRef.current += 1;
|
|
75
|
+
},
|
|
76
|
+
reset,
|
|
77
|
+
};
|
|
78
|
+
}
|