@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
|
@@ -16,7 +16,8 @@ import { withRetry } from '../services/retry';
|
|
|
16
16
|
import { mapToKycError, safeReportError } from '../services/errors';
|
|
17
17
|
import { compressSelfieImage, compressVideo } from '../services/mediaCompress';
|
|
18
18
|
import { KYCError } from '../types/verification';
|
|
19
|
-
import {
|
|
19
|
+
import { useStore } from 'zustand';
|
|
20
|
+
import { useKyc, useKycConfig, useKycStore, useTheme } from '../components/runtime';
|
|
20
21
|
import { MyazaText } from '../components/Typography';
|
|
21
22
|
import { MyazaButton } from '../components/MyazaButton';
|
|
22
23
|
import { MyazaPulseLoader } from '../components/MyazaPulseLoader';
|
|
@@ -30,8 +31,26 @@ import {
|
|
|
30
31
|
import { Icon } from '../components/Icon';
|
|
31
32
|
import { useToast } from '../components/toast';
|
|
32
33
|
import { CameraPermissionView, CameraUnavailableView, CameraPermissionPrimingView } from '../components/CameraPermissionView';
|
|
34
|
+
import { ReadyPrimer } from '../components/ReadyPrimer';
|
|
35
|
+
import { READY_LIVENESS } from '../components/readyPrimerContent';
|
|
33
36
|
import { LivenessAvatar } from './LivenessAvatar';
|
|
34
37
|
import { detectFaceOnFrame } from '../liveness/visionCameraFaceDetector';
|
|
38
|
+
import { buildLivenessIntegrity } from '../liveness/integritySignals';
|
|
39
|
+
import { DEFAULT_LIVENESS_CONFIG } from '../liveness/types';
|
|
40
|
+
import { useFlashSequence } from './useFlashSequence';
|
|
41
|
+
import { useFlashHole } from './useFlashHole';
|
|
42
|
+
import {
|
|
43
|
+
FRESH_FACE_TIMEOUT_MS,
|
|
44
|
+
INSTRUCTION_HEIGHT,
|
|
45
|
+
LightingBanner,
|
|
46
|
+
ProgressDots,
|
|
47
|
+
SelfiePreview,
|
|
48
|
+
StyleAbsFill,
|
|
49
|
+
resolveGuidance,
|
|
50
|
+
useSelfieUpload,
|
|
51
|
+
LivenessComplete,
|
|
52
|
+
LivenessFailed,
|
|
53
|
+
} from './liveness';
|
|
35
54
|
import {
|
|
36
55
|
lightingGuidanceText,
|
|
37
56
|
positionGuidanceText,
|
|
@@ -63,6 +82,7 @@ export function LivenessStep(): React.ReactElement {
|
|
|
63
82
|
const config = useKycConfig();
|
|
64
83
|
const api = useKyc((s) => s.api);
|
|
65
84
|
const setMediaId = useKyc((s) => s.setMediaId);
|
|
85
|
+
const setCaptureIntegrity = useKyc((s) => s.setCaptureIntegrity);
|
|
66
86
|
const nextStep = useKyc((s) => s.nextStep);
|
|
67
87
|
|
|
68
88
|
const device = useCameraDevice('front');
|
|
@@ -102,6 +122,8 @@ export function LivenessStep(): React.ReactElement {
|
|
|
102
122
|
return () => clearTimeout(t);
|
|
103
123
|
}, []);
|
|
104
124
|
const cameraUnavailable = cameraGrace && !device;
|
|
125
|
+
// Gate the whole camera path on the user acknowledging the primer.
|
|
126
|
+
const [ready, setReady] = useState(false);
|
|
105
127
|
const showPrimer = perm === 'priming' && !!device;
|
|
106
128
|
const permissionDenied = perm === 'denied' && !!device;
|
|
107
129
|
|
|
@@ -134,56 +156,19 @@ export function LivenessStep(): React.ReactElement {
|
|
|
134
156
|
if (!permissionDenied) permReportedRef.current = false;
|
|
135
157
|
}, [permissionDenied, config.onError]);
|
|
136
158
|
|
|
137
|
-
//
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
setUploadError(null);
|
|
151
|
-
setRetryInfo(null);
|
|
152
|
-
const onRetry = (attempt: number, total: number) => setRetryInfo({ attempt, total });
|
|
153
|
-
try {
|
|
154
|
-
const selfieId = await withRetry(
|
|
155
|
-
() => api.upload({ uri: selfie, type: 'image/jpeg' }, 'selfie'),
|
|
156
|
-
{ onRetry },
|
|
157
|
-
);
|
|
158
|
-
selfieIdRef.current = selfieId;
|
|
159
|
-
setMediaId('selfie', selfieId);
|
|
160
|
-
// Liveness video is best-effort — a failure here must not block the user.
|
|
161
|
-
if (videoPath) {
|
|
162
|
-
try {
|
|
163
|
-
// Transcode the raw recording down to a small evidence clip first.
|
|
164
|
-
const small = await compressVideo(videoPath);
|
|
165
|
-
const videoId = await withRetry(
|
|
166
|
-
() => api.upload({ uri: small, type: 'video/mp4' }, 'liveness_video', MAX_VIDEO_BYTES),
|
|
167
|
-
{ onRetry },
|
|
168
|
-
);
|
|
169
|
-
setMediaId('livenessVideo', videoId);
|
|
170
|
-
} catch {
|
|
171
|
-
/* keep the selfie, drop the video (incl. if it exceeded the 5MB cap) */
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
setRetryInfo(null);
|
|
175
|
-
setUploading(false);
|
|
176
|
-
} catch (err) {
|
|
177
|
-
setRetryInfo(null);
|
|
178
|
-
setUploading(false);
|
|
179
|
-
const kycError = mapToKycError(err, 'upload');
|
|
180
|
-
setUploadError(kycError.message);
|
|
181
|
-
toast.show({ variant: 'error', title: 'Upload failed', message: kycError.message });
|
|
182
|
-
safeReportError(config.onError, kycError);
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
[api, setMediaId, config.onError, toast],
|
|
186
|
-
);
|
|
159
|
+
// Selfie + liveness-video upload (see ./liveness/useSelfieUpload).
|
|
160
|
+
const upload = useSelfieUpload();
|
|
161
|
+
const {
|
|
162
|
+
selfieUri,
|
|
163
|
+
setSelfieUri,
|
|
164
|
+
uploading,
|
|
165
|
+
retryInfo,
|
|
166
|
+
uploadError,
|
|
167
|
+
setUploadError,
|
|
168
|
+
selfieIdRef,
|
|
169
|
+
videoPathRef,
|
|
170
|
+
uploadSelfieAndVideo,
|
|
171
|
+
} = upload;
|
|
187
172
|
|
|
188
173
|
// ── Liveness state machine ─────────────────────────────────────────────────
|
|
189
174
|
// `handleCapture` is created after `liveness` (it calls liveness.markComplete),
|
|
@@ -191,7 +176,16 @@ export function LivenessStep(): React.ReactElement {
|
|
|
191
176
|
const captureRef = useRef<() => void>(() => {});
|
|
192
177
|
const liveness = useLiveness({
|
|
193
178
|
voiceGuidance: config.voiceGuidance,
|
|
179
|
+
// Silent until the primer is dismissed and the camera is actually up. The
|
|
180
|
+
// machine is constructed behind the primer, so without this it talks to a
|
|
181
|
+
// user who has not started yet.
|
|
182
|
+
announce: ready && !showPrimer,
|
|
194
183
|
onReadyToCapture: () => captureRef.current(),
|
|
184
|
+
config: {
|
|
185
|
+
...DEFAULT_LIVENESS_CONFIG,
|
|
186
|
+
mode: config.livenessMode ?? 'gestures',
|
|
187
|
+
flashSequenceLength: config.flashSequenceLength,
|
|
188
|
+
},
|
|
195
189
|
});
|
|
196
190
|
|
|
197
191
|
const handleCapture = useCallback(async () => {
|
|
@@ -206,10 +200,46 @@ export function LivenessStep(): React.ReactElement {
|
|
|
206
200
|
await new Promise((r) => setTimeout(r, SELFIE_SETTLE_MS));
|
|
207
201
|
// v5: capture the selfie via the photo output (no camera ref). filePath is
|
|
208
202
|
// a plain fs path — prepend file:// for the uploader/compressor.
|
|
203
|
+
// A different face appeared while the machine was in `capturing`. The
|
|
204
|
+
// verdict was recorded rather than shown (nothing may re-lay out mid
|
|
205
|
+
// capture), so it is acted on here — BEFORE the shutter, so no still of a
|
|
206
|
+
// stranger is ever taken, let alone uploaded.
|
|
207
|
+
// Live read. `liveness.integrityBroken` is a snapshot from the render
|
|
208
|
+
// this callback was created in — i.e. from BEFORE the flash — so a face
|
|
209
|
+
// swapped or added during the sequence was recorded and then never seen
|
|
210
|
+
// here.
|
|
211
|
+
if (liveness.isCompromised()) {
|
|
212
|
+
liveness.reportIntegrityFailure();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// Nobody in frame? Do not photograph an empty room.
|
|
216
|
+
//
|
|
217
|
+
// The flash can leave the face briefly undetectable, so this waits for a
|
|
218
|
+
// FRESH detection now that the overlay is gone rather than trusting a
|
|
219
|
+
// timestamp from before it started — a stale stamp from just before the
|
|
220
|
+
// user walked away passes any recency check, which is precisely how a
|
|
221
|
+
// blank selfie got captured and submitted as a passing liveness result.
|
|
222
|
+
//
|
|
223
|
+
// Losing the face is not a failure, just an abandoned attempt: drop back
|
|
224
|
+
// to positioning with the camera still running, so the user re-frames and
|
|
225
|
+
// the gate fires again with no teardown and no permission re-prompt.
|
|
226
|
+
if (!(await liveness.awaitFreshFace(FRESH_FACE_TIMEOUT_MS))) {
|
|
227
|
+
liveness.reset();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
209
230
|
const file = await photoOutput.capturePhotoToFile({ flashMode: 'off' }, {});
|
|
210
231
|
const raw = `file://${file.filePath}`;
|
|
211
232
|
const compressed = await compressSelfieImage(raw).catch(() => raw);
|
|
212
233
|
setSelfieUri(compressed);
|
|
234
|
+
// Recorded at capture, not at submit: by then the liveness hook is gone
|
|
235
|
+
// and its flash result with it.
|
|
236
|
+
setCaptureIntegrity({
|
|
237
|
+
liveness: buildLivenessIntegrity(
|
|
238
|
+
config.livenessMode ?? 'gestures',
|
|
239
|
+
liveness.faceGlitches,
|
|
240
|
+
liveness.flashResult,
|
|
241
|
+
),
|
|
242
|
+
});
|
|
213
243
|
liveness.markComplete();
|
|
214
244
|
// Stop the liveness video (best-effort) and start the eager upload. The
|
|
215
245
|
// recorder resolves the finished file path (or null if nothing recorded);
|
|
@@ -220,7 +250,7 @@ export function LivenessStep(): React.ReactElement {
|
|
|
220
250
|
} catch {
|
|
221
251
|
liveness.reset();
|
|
222
252
|
}
|
|
223
|
-
}, [device, liveness, uploadSelfieAndVideo, photoOutput, videoRecorder]);
|
|
253
|
+
}, [device, liveness, uploadSelfieAndVideo, photoOutput, videoRecorder, setCaptureIntegrity, config.livenessMode]);
|
|
224
254
|
|
|
225
255
|
useEffect(() => {
|
|
226
256
|
captureRef.current = () => void handleCapture();
|
|
@@ -242,20 +272,61 @@ export function LivenessStep(): React.ReactElement {
|
|
|
242
272
|
// with `runOnJS(...)` INSIDE the worklet (the canonical react-native-worklets
|
|
243
273
|
// pattern) rather than pre-wrapped — pre-wrapping produced a "non-worklet
|
|
244
274
|
// function" error when called from the frame-processor worklet.
|
|
275
|
+
// The most recent face-region RGB, for the flash sequence. A ref rather than
|
|
276
|
+
// state: this updates every frame, and re-rendering the camera preview at
|
|
277
|
+
// frame rate would be both wasteful and — during a flash — actively harmful,
|
|
278
|
+
// since the overlay's cutout is measured once.
|
|
279
|
+
const faceRgbRef = useRef<readonly [number, number, number] | null>(null);
|
|
280
|
+
|
|
245
281
|
const handleFace = useCallback((data: LivenessFaceData | null) => {
|
|
246
282
|
const l = livenessRef.current;
|
|
247
283
|
if (!data) {
|
|
284
|
+
faceRgbRef.current = null;
|
|
248
285
|
l.onNoFace();
|
|
249
286
|
return;
|
|
250
287
|
}
|
|
288
|
+
faceRgbRef.current = data.faceRgb ?? null;
|
|
251
289
|
// Low-light gate (thresholds mirror Flutter's _BrightnessSampler: <62 dark,
|
|
252
290
|
// >200 bright). Feeds the lighting warning + speech and blocks challenges so
|
|
253
291
|
// gestures don't misbehave in poor light.
|
|
254
292
|
l.setLighting(data.brightness < 62 ? 'dark' : data.brightness > 200 ? 'bright' : null);
|
|
255
293
|
if (data.faceCount > 0) l.onFace(data);
|
|
256
|
-
else
|
|
294
|
+
else {
|
|
295
|
+
faceRgbRef.current = null;
|
|
296
|
+
l.onNoFace();
|
|
297
|
+
}
|
|
257
298
|
}, []);
|
|
258
299
|
|
|
300
|
+
|
|
301
|
+
// Hoisted above this component's several early returns — it is a hook, and
|
|
302
|
+
// calling it further down (next to the circle it measures, where it reads
|
|
303
|
+
// better) would make it conditional on which branch rendered.
|
|
304
|
+
const { ref: flashHoleRef, hole: flashHole } = useFlashHole(liveness.phase === 'flash');
|
|
305
|
+
|
|
306
|
+
const { flashColor } = useFlashSequence({
|
|
307
|
+
active: liveness.shouldFlash,
|
|
308
|
+
// Stop painting the moment a second face appears or the face is swapped:
|
|
309
|
+
// finishing the sequence would produce a reflection measurement for a face
|
|
310
|
+
// that is no longer the subject.
|
|
311
|
+
shouldContinue: () => !livenessRef.current.isCompromised(),
|
|
312
|
+
sequenceLength: config.flashSequenceLength,
|
|
313
|
+
readFaceRgb: () => faceRgbRef.current,
|
|
314
|
+
onComplete: (result) => livenessRef.current.completeFlash(result),
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Published to the sheet root rather than drawn in this step: an overlay here
|
|
318
|
+
// lights only the padded body, and the screen IS the light source. The sheet
|
|
319
|
+
// root is still the SAME tree as the preview, so the cutout and the preview
|
|
320
|
+
// scale together under any UIKit transform.
|
|
321
|
+
const flashStore = useKycStore();
|
|
322
|
+
const setFlashPaint = useStore(flashStore, (st) => st.setFlashPaint);
|
|
323
|
+
const isFlashing = liveness.phase === 'flash';
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
setFlashPaint(isFlashing ? { color: flashColor, hole: flashHole } : null);
|
|
326
|
+
}, [isFlashing, flashColor, flashHole, setFlashPaint]);
|
|
327
|
+
// Clear on unmount so a colour can never outlive the step.
|
|
328
|
+
useEffect(() => () => setFlashPaint(null), [setFlashPaint]);
|
|
329
|
+
|
|
259
330
|
const frameOutput = useFrameOutput({
|
|
260
331
|
// ML Kit (Android) and Apple Vision both consume YUV efficiently.
|
|
261
332
|
pixelFormat: 'yuv',
|
|
@@ -280,6 +351,13 @@ export function LivenessStep(): React.ReactElement {
|
|
|
280
351
|
if (cameraUnavailable) {
|
|
281
352
|
return <CameraUnavailableView />;
|
|
282
353
|
}
|
|
354
|
+
// "Here's what happens next", BEFORE the OS prompt — and shown even when
|
|
355
|
+
// permission is already granted, because it is about what the step will ask
|
|
356
|
+
// of the user, not about access. Opening the camera unannounced is what makes
|
|
357
|
+
// people fumble the first attempt.
|
|
358
|
+
if (!ready) {
|
|
359
|
+
return <ReadyPrimer content={READY_LIVENESS} onReady={() => setReady(true)} />;
|
|
360
|
+
}
|
|
283
361
|
if (showPrimer) {
|
|
284
362
|
return <CameraPermissionPrimingView onGrant={() => setPerm('requesting')} />;
|
|
285
363
|
}
|
|
@@ -290,67 +368,23 @@ export function LivenessStep(): React.ReactElement {
|
|
|
290
368
|
// ── Review (selfie captured) ─────────────────────────────────────────────────
|
|
291
369
|
if (liveness.phase === 'complete' && selfieUri) {
|
|
292
370
|
return (
|
|
293
|
-
<
|
|
294
|
-
|
|
295
|
-
{
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
loading={uploading}
|
|
306
|
-
onPress={() => void uploadSelfieAndVideo(selfieUri, videoPathRef.current)}
|
|
307
|
-
/>
|
|
308
|
-
) : (
|
|
309
|
-
<View style={{ flexDirection: 'row', gap: spacing.md }}>
|
|
310
|
-
<View style={{ flex: 1 }}>
|
|
311
|
-
<MyazaButton
|
|
312
|
-
label="Retake"
|
|
313
|
-
variant="outline"
|
|
314
|
-
leadingIcon="refresh"
|
|
315
|
-
disabled={uploading}
|
|
316
|
-
onPress={() => {
|
|
317
|
-
setSelfieUri(null);
|
|
318
|
-
setUploadError(null);
|
|
319
|
-
selfieIdRef.current = null;
|
|
320
|
-
videoPathRef.current = null;
|
|
321
|
-
liveness.reset();
|
|
322
|
-
}}
|
|
323
|
-
/>
|
|
324
|
-
</View>
|
|
325
|
-
<View style={{ flex: 1 }}>
|
|
326
|
-
<MyazaButton
|
|
327
|
-
label="Continue"
|
|
328
|
-
loading={uploading}
|
|
329
|
-
disabled={uploading || !selfieIdRef.current}
|
|
330
|
-
onPress={nextStep}
|
|
331
|
-
/>
|
|
332
|
-
</View>
|
|
333
|
-
</View>
|
|
334
|
-
)}
|
|
335
|
-
</View>
|
|
371
|
+
<LivenessComplete
|
|
372
|
+
selfieUri={selfieUri}
|
|
373
|
+
upload={upload}
|
|
374
|
+
onRetake={() => {
|
|
375
|
+
setSelfieUri(null);
|
|
376
|
+
setUploadError(null);
|
|
377
|
+
selfieIdRef.current = null;
|
|
378
|
+
videoPathRef.current = null;
|
|
379
|
+
liveness.reset();
|
|
380
|
+
}}
|
|
381
|
+
onContinue={nextStep}
|
|
382
|
+
/>
|
|
336
383
|
);
|
|
337
384
|
}
|
|
338
385
|
|
|
339
|
-
// ── Failed (timeout / face lost) — mirrors the Flutter failed view: a centred
|
|
340
|
-
// red message and a full-width "Try Again" button (no icon). ───────────────
|
|
341
386
|
if (liveness.phase === 'failed') {
|
|
342
|
-
return
|
|
343
|
-
<View style={{ alignItems: 'center', justifyContent: 'center', paddingVertical: spacing.xl, gap: spacing.lg }}>
|
|
344
|
-
<MyazaText variant="bodyMedium" color={colors.error} style={{ textAlign: 'center', fontWeight: '500' }}>
|
|
345
|
-
{liveness.failureReason === 'timeout'
|
|
346
|
-
? "Time's up. Let's try again."
|
|
347
|
-
: 'Face lost. Please try again.'}
|
|
348
|
-
</MyazaText>
|
|
349
|
-
<View style={{ alignSelf: 'stretch' }}>
|
|
350
|
-
<MyazaButton label="Try Again" onPress={liveness.reset} />
|
|
351
|
-
</View>
|
|
352
|
-
</View>
|
|
353
|
-
);
|
|
387
|
+
return <LivenessFailed reason={liveness.failureReason} onRetry={liveness.reset} />;
|
|
354
388
|
}
|
|
355
389
|
|
|
356
390
|
// ── Live camera (circular) + ring state machine + guidance ──────────────────
|
|
@@ -391,15 +425,23 @@ export function LivenessStep(): React.ReactElement {
|
|
|
391
425
|
const CIRCLE = Math.min(Dimensions.get('window').width - spacing.md * 4, 300);
|
|
392
426
|
const lighting = liveness.lightingGuidance;
|
|
393
427
|
|
|
428
|
+
// Rendered INSIDE the sheet, so the SDK header and the "powered by" footer
|
|
429
|
+
// stay visible throughout — only the flash itself goes full-screen.
|
|
394
430
|
return (
|
|
395
431
|
<View style={{ alignItems: 'center', gap: spacing.md }}>
|
|
396
432
|
{/* Instruction text — above the circle */}
|
|
397
|
-
<MyazaText
|
|
398
|
-
|
|
433
|
+
<MyazaText
|
|
434
|
+
variant="heading3"
|
|
435
|
+
style={{ textAlign: 'center', minHeight: INSTRUCTION_HEIGHT }}
|
|
436
|
+
color={instrColor}
|
|
437
|
+
>
|
|
438
|
+
{phase === 'flash' ? '' : guidance.text}
|
|
399
439
|
</MyazaText>
|
|
400
440
|
|
|
401
441
|
{/* Circular camera with a thick colour-state ring */}
|
|
402
442
|
<View
|
|
443
|
+
ref={flashHoleRef}
|
|
444
|
+
collapsable={false}
|
|
403
445
|
style={{
|
|
404
446
|
width: CIRCLE,
|
|
405
447
|
height: CIRCLE,
|
|
@@ -478,144 +520,10 @@ export function LivenessStep(): React.ReactElement {
|
|
|
478
520
|
{lighting ? <LightingBanner text={lightingGuidanceText(lighting)} /> : null}
|
|
479
521
|
|
|
480
522
|
{/* Numbered progress dots with connectors */}
|
|
481
|
-
<ProgressDots total={liveness.totalCount} completed={liveness.completedCount} active={phase === 'challenge' || phase === 'positioning'} />
|
|
523
|
+
<ProgressDots total={liveness.totalCount} completed={liveness.completedCount} active={phase === 'challenge' || phase === 'positioning' || phase === 'flash'} />
|
|
482
524
|
|
|
483
525
|
{/* Gesture demo avatar */}
|
|
484
526
|
{liveness.activeChallenge ? <LivenessAvatar challenge={liveness.activeChallenge} /> : null}
|
|
485
527
|
</View>
|
|
486
528
|
);
|
|
487
529
|
}
|
|
488
|
-
|
|
489
|
-
const StyleAbsFill = { position: 'absolute' as const, top: 0, left: 0, right: 0, bottom: 0 };
|
|
490
|
-
|
|
491
|
-
// Lighting warning — 1:1 with the Flutter `_LightingWarningBanner`: amber-50 bg,
|
|
492
|
-
// amber-200 border, amber-800 lightbulb + text, fading/sliding in (~300ms).
|
|
493
|
-
const AMBER_50 = '#FFFBEB';
|
|
494
|
-
const AMBER_200 = '#FDE68A';
|
|
495
|
-
const AMBER_800 = '#92400E';
|
|
496
|
-
|
|
497
|
-
function LightingBanner({ text }: { text: string }): React.ReactElement {
|
|
498
|
-
const anim = useRef(new Animated.Value(0)).current;
|
|
499
|
-
useEffect(() => {
|
|
500
|
-
Animated.timing(anim, { toValue: 1, duration: 300, easing: Easing.out(Easing.cubic), useNativeDriver: true }).start();
|
|
501
|
-
}, [anim]);
|
|
502
|
-
return (
|
|
503
|
-
<Animated.View
|
|
504
|
-
style={{
|
|
505
|
-
alignSelf: 'stretch',
|
|
506
|
-
opacity: anim,
|
|
507
|
-
transform: [{ translateY: anim.interpolate({ inputRange: [0, 1], outputRange: [-8, 0] }) }],
|
|
508
|
-
}}
|
|
509
|
-
>
|
|
510
|
-
<View
|
|
511
|
-
style={{
|
|
512
|
-
flexDirection: 'row',
|
|
513
|
-
alignItems: 'flex-start',
|
|
514
|
-
gap: spacing.sm,
|
|
515
|
-
borderRadius: radius.sm,
|
|
516
|
-
borderWidth: 1,
|
|
517
|
-
borderColor: AMBER_200,
|
|
518
|
-
backgroundColor: AMBER_50,
|
|
519
|
-
paddingHorizontal: spacing.sm + 4,
|
|
520
|
-
paddingVertical: 10,
|
|
521
|
-
}}
|
|
522
|
-
>
|
|
523
|
-
<Icon name="lightbulb" size={16} color={AMBER_800} />
|
|
524
|
-
<MyazaText variant="bodySmall" color={AMBER_800} style={{ flexShrink: 1, lineHeight: 17 }}>
|
|
525
|
-
{text}
|
|
526
|
-
</MyazaText>
|
|
527
|
-
</View>
|
|
528
|
-
</Animated.View>
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/** The single guidance string + tone to show under the camera. */
|
|
533
|
-
function resolveGuidance(l: ReturnType<typeof useLiveness>): { text: string; tone: 'normal' | 'error' } {
|
|
534
|
-
if (l.multipleFaces) return { text: l.instruction, tone: 'error' };
|
|
535
|
-
// Lighting is shown in its own banner (below), not as the main instruction.
|
|
536
|
-
if (l.phase === 'positioning' && !l.faceDetected) {
|
|
537
|
-
return { text: 'Position your face in the circle', tone: 'normal' };
|
|
538
|
-
}
|
|
539
|
-
if (l.positionGuidance) return { text: positionGuidanceText(l.positionGuidance), tone: 'error' };
|
|
540
|
-
if (l.wrongGesture) return { text: 'Wrong gesture — follow the prompt', tone: 'error' };
|
|
541
|
-
return { text: l.instruction, tone: 'normal' };
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Numbered progress steps with connectors — mirrors the web/Flutter SDKs:
|
|
545
|
-
// passed = solid green + checkmark, active = green-outlined number, pending =
|
|
546
|
-
// grey number. `active` marks the current (completed-th) step as in-progress.
|
|
547
|
-
function ProgressDots({
|
|
548
|
-
total,
|
|
549
|
-
completed,
|
|
550
|
-
active,
|
|
551
|
-
}: {
|
|
552
|
-
total: number;
|
|
553
|
-
completed: number;
|
|
554
|
-
active: boolean;
|
|
555
|
-
}): React.ReactElement {
|
|
556
|
-
const { colors } = useTheme();
|
|
557
|
-
return (
|
|
558
|
-
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }}>
|
|
559
|
-
{Array.from({ length: total }).map((_, i) => {
|
|
560
|
-
const state: 'passed' | 'active' | 'pending' =
|
|
561
|
-
i < completed ? 'passed' : i === completed && active ? 'active' : 'pending';
|
|
562
|
-
return (
|
|
563
|
-
<React.Fragment key={i}>
|
|
564
|
-
<View
|
|
565
|
-
style={{
|
|
566
|
-
width: 28,
|
|
567
|
-
height: 28,
|
|
568
|
-
borderRadius: 14,
|
|
569
|
-
alignItems: 'center',
|
|
570
|
-
justifyContent: 'center',
|
|
571
|
-
backgroundColor: state === 'passed' ? colors.success : state === 'active' ? colors.background : colors.gray300,
|
|
572
|
-
borderWidth: state === 'active' ? 2 : 0,
|
|
573
|
-
borderColor: colors.success,
|
|
574
|
-
}}
|
|
575
|
-
>
|
|
576
|
-
{state === 'passed' ? (
|
|
577
|
-
<Svg width={14} height={14} viewBox="0 0 24 24">
|
|
578
|
-
<Path d="M5 13l4 4L19 7" fill="none" stroke="#FFFFFF" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round" />
|
|
579
|
-
</Svg>
|
|
580
|
-
) : (
|
|
581
|
-
<MyazaText variant="bodySmall" color={state === 'active' ? colors.success : colors.textMuted} style={{ fontWeight: '700' }}>
|
|
582
|
-
{String(i + 1)}
|
|
583
|
-
</MyazaText>
|
|
584
|
-
)}
|
|
585
|
-
</View>
|
|
586
|
-
{i < total - 1 ? (
|
|
587
|
-
<View style={{ width: 28, height: 2, backgroundColor: i < completed ? colors.success : colors.gray300 }} />
|
|
588
|
-
) : null}
|
|
589
|
-
</React.Fragment>
|
|
590
|
-
);
|
|
591
|
-
})}
|
|
592
|
-
</View>
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function SelfiePreview({ uri, uploading }: { uri: string; uploading?: boolean }): React.ReactElement {
|
|
597
|
-
const { colors } = useTheme();
|
|
598
|
-
const S = 200;
|
|
599
|
-
return (
|
|
600
|
-
<View style={{ alignItems: 'center', justifyContent: 'center', alignSelf: 'stretch' }}>
|
|
601
|
-
<View style={{ width: S, height: S, borderRadius: S / 2, borderWidth: 4, backgroundColor: '#111111', borderColor: `${colors.primary}33`, overflow: 'hidden' }}>
|
|
602
|
-
{/* borderRadius repeated on the Image — iOS doesn't reliably clip an Image
|
|
603
|
-
child to a rounded parent (same fix as the header brand bar). */}
|
|
604
|
-
<Image source={{ uri }} style={{ width: '100%', height: '100%', borderRadius: S / 2 }} resizeMode="cover" />
|
|
605
|
-
{/* Standard upload loader inside the preview circle (mirrors web/Flutter). */}
|
|
606
|
-
{uploading ? (
|
|
607
|
-
<View style={[StyleAbsFill, { backgroundColor: 'rgba(0,0,0,0.45)', alignItems: 'center', justifyContent: 'center' }]}>
|
|
608
|
-
<MyazaPulseLoader size={64} />
|
|
609
|
-
</View>
|
|
610
|
-
) : null}
|
|
611
|
-
</View>
|
|
612
|
-
<View style={{ height: spacing.md }} />
|
|
613
|
-
<MyazaText variant="heading3" style={{ textAlign: 'center' }}>
|
|
614
|
-
Looking good!
|
|
615
|
-
</MyazaText>
|
|
616
|
-
<MyazaText variant="bodySmall" color={colors.textSecondary} style={{ textAlign: 'center' }}>
|
|
617
|
-
Tap Continue to submit, or Retake to try again.
|
|
618
|
-
</MyazaText>
|
|
619
|
-
</View>
|
|
620
|
-
);
|
|
621
|
-
}
|