@myazahq/kyc-sdk-react-native 2.0.2 → 2.2.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 +152 -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 +70 -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 +162 -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 +322 -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 +207 -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/KeyPersonCard.tsx +119 -0
- package/src/screens/KeyPersonForm.tsx +148 -0
- package/src/screens/KeyPersonSheet.tsx +184 -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,109 @@
|
|
|
1
|
+
import type { MyazaKYCConfig } from '../../types/config';
|
|
2
|
+
import type { IconName } from '../../components/Icon';
|
|
3
|
+
import { fillTokens } from '../../utils/tokens';
|
|
4
|
+
import { hasEmailVerificationStep, hasPhoneVerificationStep } from '../../config/contact';
|
|
5
|
+
import {
|
|
6
|
+
hasApplicantVerification,
|
|
7
|
+
hasBusinessDocumentsStep,
|
|
8
|
+
hasKeyPeopleCollection,
|
|
9
|
+
} from '../../config/businessSteps';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// What the consent screen SAYS, derived from what the flow actually DOES.
|
|
13
|
+
//
|
|
14
|
+
// Mirrors the web SDK's ConsentStep derivations exactly — a KYB flow gets the
|
|
15
|
+
// business title, description, step list and data clause, never the identity
|
|
16
|
+
// copy ("verify your government-issued ID" on a registry-lookup flow is simply
|
|
17
|
+
// false). The biometric sentence is derived, not assumed: claiming facial
|
|
18
|
+
// recognition on a flow with no face capture would be a false statement in a
|
|
19
|
+
// legal notice, and recording video without saying so is the failure that
|
|
20
|
+
// actually carries risk.
|
|
21
|
+
//
|
|
22
|
+
// Pure (no RN imports beyond types) so it is unit-testable next to the web
|
|
23
|
+
// SDK's consent-disclosure tests. The screen just renders this.
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export interface ConsentProcessStep {
|
|
27
|
+
icon: IconName;
|
|
28
|
+
label: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ConsentModel {
|
|
32
|
+
isBusiness: boolean;
|
|
33
|
+
title: string;
|
|
34
|
+
description: string;
|
|
35
|
+
steps: ConsentProcessStep[];
|
|
36
|
+
/** Drives the "facial recognition" sentence in the legal notice. */
|
|
37
|
+
capturesFace: boolean;
|
|
38
|
+
/** Drives the "recording this session" sentence. */
|
|
39
|
+
recordsVideo: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DEFAULT_DESCRIPTION =
|
|
43
|
+
'We need to verify your identity to comply with regulatory requirements. This process is quick and secure.';
|
|
44
|
+
|
|
45
|
+
const DEFAULT_BUSINESS_DESCRIPTION =
|
|
46
|
+
'We need to verify your business to comply with regulatory requirements. This process is quick and secure.';
|
|
47
|
+
|
|
48
|
+
export function buildConsentModel(config: MyazaKYCConfig): ConsentModel {
|
|
49
|
+
const isBusiness = config.subjectType === 'business';
|
|
50
|
+
const firstName = config.userData?.firstName ?? '';
|
|
51
|
+
|
|
52
|
+
const defaultTitle = firstName
|
|
53
|
+
? `Welcome, ${firstName}`
|
|
54
|
+
: isBusiness
|
|
55
|
+
? 'Business Verification'
|
|
56
|
+
: 'Identity Verification';
|
|
57
|
+
const title = config.consent?.title
|
|
58
|
+
? fillTokens(config.consent.title, config.userData)
|
|
59
|
+
: defaultTitle;
|
|
60
|
+
const description = config.consent?.description
|
|
61
|
+
? fillTokens(config.consent.description, config.userData)
|
|
62
|
+
: isBusiness
|
|
63
|
+
? DEFAULT_BUSINESS_DESCRIPTION
|
|
64
|
+
: DEFAULT_DESCRIPTION;
|
|
65
|
+
|
|
66
|
+
// A business flow captures a face only when the applicant verifies their own
|
|
67
|
+
// identity in-flow; an individual flow whenever the selfie step is on.
|
|
68
|
+
const capturesFace = isBusiness
|
|
69
|
+
? hasApplicantVerification(config.business)
|
|
70
|
+
: config.enableSelfie !== false;
|
|
71
|
+
const recordsVideo = capturesFace || (!isBusiness && config.enableDocumentCapture !== false);
|
|
72
|
+
|
|
73
|
+
const steps: ConsentProcessStep[] = isBusiness
|
|
74
|
+
? [
|
|
75
|
+
{ icon: 'building-2', label: 'Collect your business registration details' },
|
|
76
|
+
{ icon: 'badge-check', label: 'Verify your business against the official registry' },
|
|
77
|
+
]
|
|
78
|
+
: [
|
|
79
|
+
{ icon: 'badge-check', label: 'Verify your government-issued ID' },
|
|
80
|
+
{ icon: 'user', label: 'Collect basic personal information' },
|
|
81
|
+
];
|
|
82
|
+
if (hasEmailVerificationStep(config.emailVerification) || hasPhoneVerificationStep(config.phoneVerification)) {
|
|
83
|
+
steps.push({ icon: 'lock', label: 'Confirm your contact details with a one-time code' });
|
|
84
|
+
}
|
|
85
|
+
if (!isBusiness && config.enableDocumentCapture !== false) {
|
|
86
|
+
steps.push({ icon: 'scan-line', label: 'Capture a photo of your ID document' });
|
|
87
|
+
}
|
|
88
|
+
// Chip-capable IDs additionally read the document's NFC chip — listed when
|
|
89
|
+
// the flow enables NFC so the user knows to have the physical document to
|
|
90
|
+
// hand (a non-chip ID simply skips the step). Individual flows only; a KYB
|
|
91
|
+
// config can't carry `nfc`.
|
|
92
|
+
if (!isBusiness && config.nfc?.enabled) {
|
|
93
|
+
steps.push({ icon: 'nfc', label: 'Scan your document’s security chip (NFC)' });
|
|
94
|
+
}
|
|
95
|
+
if (!isBusiness && config.enableSelfie !== false) {
|
|
96
|
+
steps.push({ icon: 'scan-face', label: 'Take a selfie for facial verification' });
|
|
97
|
+
}
|
|
98
|
+
if (isBusiness && hasKeyPeopleCollection(config.business)) {
|
|
99
|
+
steps.push({ icon: 'users', label: "List the company's directors and owners" });
|
|
100
|
+
}
|
|
101
|
+
if (isBusiness && hasBusinessDocumentsStep(config.business)) {
|
|
102
|
+
steps.push({ icon: 'file-text', label: 'Upload supporting business documents' });
|
|
103
|
+
}
|
|
104
|
+
if (isBusiness && hasApplicantVerification(config.business)) {
|
|
105
|
+
steps.push({ icon: 'scan-face', label: 'Verify your own identity' });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { isBusiness, title, description, steps, capturesFace, recordsVideo };
|
|
109
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Modal, Pressable, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../../config/theme';
|
|
5
|
+
import { useTheme } from '../../components/runtime';
|
|
6
|
+
import { MyazaText } from '../../components/Typography';
|
|
7
|
+
import { Icon } from '../../components/Icon';
|
|
8
|
+
import { CameraViewfinder } from '../../components/CameraViewfinder';
|
|
9
|
+
import { RequiredPill } from './RequiredPill';
|
|
10
|
+
import type { MrzScan } from '../../mrz/parse';
|
|
11
|
+
|
|
12
|
+
export interface CameraPhaseProps {
|
|
13
|
+
isBack: boolean;
|
|
14
|
+
/** True once the live preview is actually on screen — drives immersive mode. */
|
|
15
|
+
cameraLive: boolean;
|
|
16
|
+
active: boolean;
|
|
17
|
+
documentLabel: string;
|
|
18
|
+
guideAspect: number;
|
|
19
|
+
isTwoSided: boolean;
|
|
20
|
+
busy: boolean;
|
|
21
|
+
allowUpload: boolean;
|
|
22
|
+
country: string;
|
|
23
|
+
idType?: string;
|
|
24
|
+
mrzAlreadyCaptured: boolean;
|
|
25
|
+
onMrz: (scan: MrzScan) => void;
|
|
26
|
+
onCapture: (uri: string, videoPath: string | null, viewAr: number) => void;
|
|
27
|
+
onUpload: () => void;
|
|
28
|
+
onBack: () => void;
|
|
29
|
+
/** Safe-area bottom inset for the full-bleed camera's controls. */
|
|
30
|
+
bottomInset?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The document-capture camera, in both of its shapes.
|
|
35
|
+
*
|
|
36
|
+
* IMMERSIVE is the normal one: the camera owns the screen, because the sheet's
|
|
37
|
+
* header, padding and scroll view are exactly what force a small viewfinder on
|
|
38
|
+
* a short phone — and a camera you have to scroll to is a broken camera. The
|
|
39
|
+
* pill and helper text go with it; the viewfinder carries that information as
|
|
40
|
+
* live overlays instead.
|
|
41
|
+
*
|
|
42
|
+
* The framed shape below it is what renders in the brief window while the OS
|
|
43
|
+
* permission prompt is open, where there is no live preview to hand the screen
|
|
44
|
+
* to yet.
|
|
45
|
+
*/
|
|
46
|
+
export function CameraPhase(p: CameraPhaseProps): React.ReactElement {
|
|
47
|
+
const { colors } = useTheme();
|
|
48
|
+
|
|
49
|
+
const viewfinder = (extra: {
|
|
50
|
+
fill?: boolean;
|
|
51
|
+
onBack?: () => void;
|
|
52
|
+
onUpload?: (() => void) | null;
|
|
53
|
+
bottomInset?: number;
|
|
54
|
+
}) => (
|
|
55
|
+
<CameraViewfinder
|
|
56
|
+
active={p.active}
|
|
57
|
+
side={p.isBack ? 'back' : 'front'}
|
|
58
|
+
documentLabel={p.documentLabel}
|
|
59
|
+
guideAspect={p.guideAspect}
|
|
60
|
+
onCapture={p.onCapture}
|
|
61
|
+
busy={p.busy}
|
|
62
|
+
// Auto-capture needs to know WHICH document it is looking for — without
|
|
63
|
+
// this it would fire on any text-dense frame.
|
|
64
|
+
country={p.country}
|
|
65
|
+
idType={p.idType}
|
|
66
|
+
// Banking the MRZ from a live frame is what spares the user scanning the
|
|
67
|
+
// same passport again at the chip step. Only the front carries one.
|
|
68
|
+
onMrz={p.isBack ? undefined : p.onMrz}
|
|
69
|
+
mrzAlreadyCaptured={p.mrzAlreadyCaptured}
|
|
70
|
+
{...extra}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (p.cameraLive) {
|
|
75
|
+
// A FULL-SCREEN modal of its own, not just full-bleed content.
|
|
76
|
+
//
|
|
77
|
+
// The SDK's sheet is an iOS pageSheet: inset from the top, rounded, with
|
|
78
|
+
// the card visible behind it. Filling that still leaves the camera in a
|
|
79
|
+
// letterboxed card — and a document held at arm's length needs every pixel.
|
|
80
|
+
//
|
|
81
|
+
// Presenting it as a Modal rather than switching the sheet's own
|
|
82
|
+
// presentationStyle matters: a Modal's children stay in the SAME React
|
|
83
|
+
// tree, so this step keeps its state (the acknowledged primer, the captured
|
|
84
|
+
// front) — where changing presentationStyle on a live modal remounts the
|
|
85
|
+
// subtree and loses it. It closes when the capture finishes, and the sheet
|
|
86
|
+
// is exactly as it was.
|
|
87
|
+
return (
|
|
88
|
+
<Modal
|
|
89
|
+
visible
|
|
90
|
+
animationType="fade"
|
|
91
|
+
presentationStyle="fullScreen"
|
|
92
|
+
statusBarTranslucent
|
|
93
|
+
onRequestClose={p.onBack}
|
|
94
|
+
>
|
|
95
|
+
<View style={{ flex: 1, backgroundColor: '#000000' }}>
|
|
96
|
+
{viewfinder({
|
|
97
|
+
fill: true,
|
|
98
|
+
onBack: p.onBack,
|
|
99
|
+
// The gallery escape stays reachable full-screen: it is the
|
|
100
|
+
// fallback when the camera will not read a worn or laminated
|
|
101
|
+
// document.
|
|
102
|
+
onUpload: p.allowUpload ? p.onUpload : null,
|
|
103
|
+
bottomInset: p.bottomInset ?? 0,
|
|
104
|
+
})}
|
|
105
|
+
</View>
|
|
106
|
+
</Modal>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<View>
|
|
112
|
+
<RequiredPill
|
|
113
|
+
documentLabel={p.documentLabel}
|
|
114
|
+
sideBadge={p.isTwoSided ? (p.isBack ? 'Back Side' : 'Front Side') : undefined}
|
|
115
|
+
stepLabel={p.isTwoSided ? (p.isBack ? 'Step 2 of 2' : 'Step 1 of 2') : undefined}
|
|
116
|
+
/>
|
|
117
|
+
{p.isBack ? (
|
|
118
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', marginTop: spacing.sm, marginBottom: spacing.xs }}>
|
|
119
|
+
<Icon name="credit-card" size={14} color={colors.primary} />
|
|
120
|
+
<View style={{ width: 4 }} />
|
|
121
|
+
<MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '500' }}>
|
|
122
|
+
Flip the card over and scan the other side
|
|
123
|
+
</MyazaText>
|
|
124
|
+
</View>
|
|
125
|
+
) : null}
|
|
126
|
+
<View style={{ height: spacing.md }} />
|
|
127
|
+
{viewfinder({})}
|
|
128
|
+
{!p.busy ? (
|
|
129
|
+
<>
|
|
130
|
+
<View style={{ height: spacing.md }} />
|
|
131
|
+
<MyazaText variant="bodySmall" style={{ textAlign: 'center' }}>
|
|
132
|
+
Tap the button to capture manually
|
|
133
|
+
</MyazaText>
|
|
134
|
+
{p.allowUpload ? (
|
|
135
|
+
<Pressable onPress={p.onUpload} style={{ marginTop: spacing.sm }}>
|
|
136
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }}>
|
|
137
|
+
<MyazaText variant="bodySmall">Having trouble? </MyazaText>
|
|
138
|
+
<Icon name="upload" size={14} color={colors.primary} />
|
|
139
|
+
<View style={{ width: 4 }} />
|
|
140
|
+
<MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>
|
|
141
|
+
Upload a photo instead
|
|
142
|
+
</MyazaText>
|
|
143
|
+
</View>
|
|
144
|
+
</Pressable>
|
|
145
|
+
) : null}
|
|
146
|
+
</>
|
|
147
|
+
) : null}
|
|
148
|
+
</View>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius, spacing } from '../../config/theme';
|
|
5
|
+
import { useTheme } from '../../components/runtime';
|
|
6
|
+
import { MyazaText } from '../../components/Typography';
|
|
7
|
+
import { Icon } from '../../components/Icon';
|
|
8
|
+
|
|
9
|
+
// "Required: {label}" pill with optional side badge + step label — mirrors the
|
|
10
|
+
// Flutter SDK's _RequiredPill on the capture screen.
|
|
11
|
+
export function RequiredPill({
|
|
12
|
+
documentLabel,
|
|
13
|
+
sideBadge,
|
|
14
|
+
stepLabel,
|
|
15
|
+
}: {
|
|
16
|
+
documentLabel: string;
|
|
17
|
+
sideBadge?: string;
|
|
18
|
+
stepLabel?: string;
|
|
19
|
+
}): React.ReactElement {
|
|
20
|
+
const { colors } = useTheme();
|
|
21
|
+
return (
|
|
22
|
+
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
23
|
+
<View
|
|
24
|
+
style={{
|
|
25
|
+
flex: 1,
|
|
26
|
+
flexDirection: 'row',
|
|
27
|
+
alignItems: 'center',
|
|
28
|
+
flexWrap: 'wrap',
|
|
29
|
+
borderWidth: 1,
|
|
30
|
+
borderColor: `${colors.primary}33`,
|
|
31
|
+
backgroundColor: `${colors.primary}0D`,
|
|
32
|
+
borderRadius: radius.md,
|
|
33
|
+
paddingHorizontal: spacing.sm + 4,
|
|
34
|
+
paddingVertical: spacing.sm,
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
<Icon name="credit-card" size={16} color={colors.primary} />
|
|
38
|
+
<View style={{ width: 8 }} />
|
|
39
|
+
<MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>
|
|
40
|
+
Required:{' '}
|
|
41
|
+
</MyazaText>
|
|
42
|
+
<MyazaText variant="bodySmall" color={colors.primary} style={{ flexShrink: 1 }}>
|
|
43
|
+
{documentLabel}
|
|
44
|
+
</MyazaText>
|
|
45
|
+
{sideBadge ? (
|
|
46
|
+
<View style={{ backgroundColor: `${colors.primary}26`, borderRadius: radius.full, paddingHorizontal: 8, paddingVertical: 2, marginLeft: 8 }}>
|
|
47
|
+
<MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', fontSize: 11 }}>
|
|
48
|
+
{sideBadge}
|
|
49
|
+
</MyazaText>
|
|
50
|
+
</View>
|
|
51
|
+
) : null}
|
|
52
|
+
</View>
|
|
53
|
+
{stepLabel ? (
|
|
54
|
+
<MyazaText variant="bodySmall" color={colors.textMuted} style={{ marginLeft: spacing.sm }}>
|
|
55
|
+
{stepLabel}
|
|
56
|
+
</MyazaText>
|
|
57
|
+
) : null}
|
|
58
|
+
</View>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { Animated, Easing, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing, radius } from '../../config/theme';
|
|
5
|
+
import { MyazaText } from '../../components/Typography';
|
|
6
|
+
import { Icon } from '../../components/Icon';
|
|
7
|
+
import { AMBER_50, AMBER_200, AMBER_800 } from './constants';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Lighting warning — 1:1 with the Flutter `_LightingWarningBanner`: amber-50
|
|
11
|
+
// background, amber-200 border, amber-800 lightbulb + text, fading and sliding
|
|
12
|
+
// in over ~300ms.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export function LightingBanner({ text }: { text: string }): React.ReactElement {
|
|
16
|
+
const anim = useRef(new Animated.Value(0)).current;
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
Animated.timing(anim, { toValue: 1, duration: 300, easing: Easing.out(Easing.cubic), useNativeDriver: true }).start();
|
|
19
|
+
}, [anim]);
|
|
20
|
+
return (
|
|
21
|
+
<Animated.View
|
|
22
|
+
style={{
|
|
23
|
+
alignSelf: 'stretch',
|
|
24
|
+
opacity: anim,
|
|
25
|
+
transform: [{ translateY: anim.interpolate({ inputRange: [0, 1], outputRange: [-8, 0] }) }],
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
<View
|
|
29
|
+
style={{
|
|
30
|
+
flexDirection: 'row',
|
|
31
|
+
alignItems: 'flex-start',
|
|
32
|
+
gap: spacing.sm,
|
|
33
|
+
borderRadius: radius.sm,
|
|
34
|
+
borderWidth: 1,
|
|
35
|
+
borderColor: AMBER_200,
|
|
36
|
+
backgroundColor: AMBER_50,
|
|
37
|
+
paddingHorizontal: spacing.sm + 4,
|
|
38
|
+
paddingVertical: 10,
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<Icon name="lightbulb" size={16} color={AMBER_800} />
|
|
42
|
+
<MyazaText variant="bodySmall" color={AMBER_800} style={{ flexShrink: 1, lineHeight: 17 }}>
|
|
43
|
+
{text}
|
|
44
|
+
</MyazaText>
|
|
45
|
+
</View>
|
|
46
|
+
</Animated.View>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The single guidance string + tone to show under the camera. */
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../../config/theme';
|
|
5
|
+
import { useTheme } from '../../components/runtime';
|
|
6
|
+
import { MyazaText } from '../../components/Typography';
|
|
7
|
+
import { MyazaButton } from '../../components/MyazaButton';
|
|
8
|
+
import { SelfiePreview } from './SelfiePreview';
|
|
9
|
+
import type { SelfieUpload } from './useSelfieUpload';
|
|
10
|
+
import type { LivenessFailureReason } from '../../liveness/types';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// The two ways the liveness step ends.
|
|
14
|
+
//
|
|
15
|
+
// COMPLETE is not "done": the selfie is uploading behind the preview, so
|
|
16
|
+
// Continue stays disabled until the media id exists. A failed upload keeps the
|
|
17
|
+
// captured selfie and offers a retry rather than sending the user back through
|
|
18
|
+
// the whole check for a network blip.
|
|
19
|
+
//
|
|
20
|
+
// FAILED mirrors the Flutter failed view — a centred red line and a full-width
|
|
21
|
+
// "Try Again", no icon.
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export function LivenessComplete({
|
|
25
|
+
selfieUri,
|
|
26
|
+
upload,
|
|
27
|
+
onRetake,
|
|
28
|
+
onContinue,
|
|
29
|
+
}: {
|
|
30
|
+
selfieUri: string;
|
|
31
|
+
upload: SelfieUpload;
|
|
32
|
+
onRetake: () => void;
|
|
33
|
+
onContinue: () => void;
|
|
34
|
+
}): React.ReactElement {
|
|
35
|
+
const { colors } = useTheme();
|
|
36
|
+
const { uploading, uploadError, retryInfo, selfieIdRef, videoPathRef, uploadSelfieAndVideo } =
|
|
37
|
+
upload;
|
|
38
|
+
return (
|
|
39
|
+
<View>
|
|
40
|
+
<SelfiePreview uri={selfieUri} uploading={uploading && !uploadError} />
|
|
41
|
+
{retryInfo && uploading ? (
|
|
42
|
+
<MyazaText variant="bodySmall" color={colors.warning} style={{ textAlign: 'center', marginTop: spacing.sm }}>
|
|
43
|
+
{`Upload failed — retrying (${retryInfo.attempt}/${retryInfo.total})…`}
|
|
44
|
+
</MyazaText>
|
|
45
|
+
) : null}
|
|
46
|
+
<View style={{ height: spacing.md }} />
|
|
47
|
+
{uploadError ? (
|
|
48
|
+
// The error message itself is shown as a top toast; keep a retry action.
|
|
49
|
+
<MyazaButton
|
|
50
|
+
label="Try Again"
|
|
51
|
+
// Disabled while the retry is in flight, not spinning — the progress
|
|
52
|
+
// indicator belongs in the selfie frame, where the work is.
|
|
53
|
+
disabled={uploading}
|
|
54
|
+
onPress={() => void uploadSelfieAndVideo(selfieUri, videoPathRef.current)}
|
|
55
|
+
/>
|
|
56
|
+
) : (
|
|
57
|
+
<View style={{ flexDirection: 'row', gap: spacing.md }}>
|
|
58
|
+
<View style={{ flex: 1 }}>
|
|
59
|
+
<MyazaButton
|
|
60
|
+
label="Retake"
|
|
61
|
+
variant="outline"
|
|
62
|
+
leadingIcon="refresh"
|
|
63
|
+
disabled={uploading}
|
|
64
|
+
onPress={onRetake}
|
|
65
|
+
/>
|
|
66
|
+
</View>
|
|
67
|
+
<View style={{ flex: 1 }}>
|
|
68
|
+
{/* No spinner on Continue, deliberately — 1:1 with Flutter.
|
|
69
|
+
|
|
70
|
+
There is already ONE progress indicator on this screen, inside
|
|
71
|
+
the selfie frame, and it is the truthful place for it: what is
|
|
72
|
+
loading is the photo, not the button. A second spinner on the
|
|
73
|
+
action implies the tap did something, when in fact the upload
|
|
74
|
+
started the moment the selfie appeared and the button has been
|
|
75
|
+
disabled since. Disabled-and-quiet says "not yet"; a spinner
|
|
76
|
+
says "working on your request", which would be a lie. */}
|
|
77
|
+
<MyazaButton
|
|
78
|
+
label="Continue"
|
|
79
|
+
disabled={uploading || !selfieIdRef.current}
|
|
80
|
+
onPress={onContinue}
|
|
81
|
+
/>
|
|
82
|
+
</View>
|
|
83
|
+
</View>
|
|
84
|
+
)}
|
|
85
|
+
</View>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function LivenessFailed({
|
|
90
|
+
reason,
|
|
91
|
+
onRetry,
|
|
92
|
+
}: {
|
|
93
|
+
reason: LivenessFailureReason | null;
|
|
94
|
+
onRetry: () => void;
|
|
95
|
+
}): React.ReactElement {
|
|
96
|
+
const { colors } = useTheme();
|
|
97
|
+
return (
|
|
98
|
+
<View style={{ alignItems: 'center', justifyContent: 'center', paddingVertical: spacing.xl, gap: spacing.lg }}>
|
|
99
|
+
<MyazaText variant="bodyMedium" color={colors.error} style={{ textAlign: 'center', fontWeight: '500' }}>
|
|
100
|
+
{reason === 'timeout'
|
|
101
|
+
? "Time's up. Let's try again."
|
|
102
|
+
: 'Face lost. Please try again.'}
|
|
103
|
+
</MyazaText>
|
|
104
|
+
<View style={{ alignSelf: 'stretch' }}>
|
|
105
|
+
<MyazaButton label="Try Again" onPress={onRetry} />
|
|
106
|
+
</View>
|
|
107
|
+
</View>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../../config/theme';
|
|
5
|
+
import { useTheme } from '../../components/runtime';
|
|
6
|
+
import Svg, { Path } from 'react-native-svg';
|
|
7
|
+
|
|
8
|
+
import { Icon } from '../../components/Icon';
|
|
9
|
+
import { MyazaText } from '../../components/Typography';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Numbered progress dots with connectors.
|
|
13
|
+
//
|
|
14
|
+
// One dot per step the user actually performs — the gestures AND the flash. The
|
|
15
|
+
// flash was omitted for a long time, so in 'both' mode the indicator read as
|
|
16
|
+
// finished while the flash was still to come, and in flash-only mode it showed
|
|
17
|
+
// no steps at all.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export function ProgressDots({
|
|
21
|
+
total,
|
|
22
|
+
completed,
|
|
23
|
+
active,
|
|
24
|
+
}: {
|
|
25
|
+
total: number;
|
|
26
|
+
completed: number;
|
|
27
|
+
active: boolean;
|
|
28
|
+
}): React.ReactElement {
|
|
29
|
+
const { colors } = useTheme();
|
|
30
|
+
return (
|
|
31
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }}>
|
|
32
|
+
{Array.from({ length: total }).map((_, i) => {
|
|
33
|
+
const state: 'passed' | 'active' | 'pending' =
|
|
34
|
+
i < completed ? 'passed' : i === completed && active ? 'active' : 'pending';
|
|
35
|
+
return (
|
|
36
|
+
<React.Fragment key={i}>
|
|
37
|
+
<View
|
|
38
|
+
style={{
|
|
39
|
+
width: 28,
|
|
40
|
+
height: 28,
|
|
41
|
+
borderRadius: 14,
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
justifyContent: 'center',
|
|
44
|
+
backgroundColor: state === 'passed' ? colors.success : state === 'active' ? colors.background : colors.gray300,
|
|
45
|
+
borderWidth: state === 'active' ? 2 : 0,
|
|
46
|
+
borderColor: colors.success,
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
{state === 'passed' ? (
|
|
50
|
+
<Svg width={14} height={14} viewBox="0 0 24 24">
|
|
51
|
+
<Path d="M5 13l4 4L19 7" fill="none" stroke="#FFFFFF" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round" />
|
|
52
|
+
</Svg>
|
|
53
|
+
) : (
|
|
54
|
+
<MyazaText variant="bodySmall" color={state === 'active' ? colors.success : colors.textMuted} style={{ fontWeight: '700' }}>
|
|
55
|
+
{String(i + 1)}
|
|
56
|
+
</MyazaText>
|
|
57
|
+
)}
|
|
58
|
+
</View>
|
|
59
|
+
{i < total - 1 ? (
|
|
60
|
+
<View style={{ width: 28, height: 2, backgroundColor: i < completed ? colors.success : colors.gray300 }} />
|
|
61
|
+
) : null}
|
|
62
|
+
</React.Fragment>
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
65
|
+
</View>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ActivityIndicator, Image, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../../config/theme';
|
|
5
|
+
import { useTheme } from '../../components/runtime';
|
|
6
|
+
import { MyazaText } from '../../components/Typography';
|
|
7
|
+
import { MyazaPulseLoader } from '../../components/MyazaPulseLoader';
|
|
8
|
+
import { StyleAbsFill } from './constants';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// The captured selfie, shown back while it uploads.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export function SelfiePreview({ uri, uploading }: { uri: string; uploading?: boolean }): React.ReactElement {
|
|
15
|
+
const { colors } = useTheme();
|
|
16
|
+
const S = 200;
|
|
17
|
+
return (
|
|
18
|
+
<View style={{ alignItems: 'center', justifyContent: 'center', alignSelf: 'stretch' }}>
|
|
19
|
+
<View style={{ width: S, height: S, borderRadius: S / 2, borderWidth: 4, backgroundColor: '#111111', borderColor: `${colors.primary}33`, overflow: 'hidden' }}>
|
|
20
|
+
{/* borderRadius repeated on the Image — iOS doesn't reliably clip an Image
|
|
21
|
+
child to a rounded parent (same fix as the header brand bar). */}
|
|
22
|
+
<Image source={{ uri }} style={{ width: '100%', height: '100%', borderRadius: S / 2 }} resizeMode="cover" />
|
|
23
|
+
{/* Standard upload loader inside the preview circle (mirrors web/Flutter). */}
|
|
24
|
+
{uploading ? (
|
|
25
|
+
<View style={[StyleAbsFill, { backgroundColor: 'rgba(0,0,0,0.45)', alignItems: 'center', justifyContent: 'center' }]}>
|
|
26
|
+
<MyazaPulseLoader size={64} />
|
|
27
|
+
</View>
|
|
28
|
+
) : null}
|
|
29
|
+
</View>
|
|
30
|
+
<View style={{ height: spacing.md }} />
|
|
31
|
+
<MyazaText variant="heading3" style={{ textAlign: 'center' }}>
|
|
32
|
+
Looking good!
|
|
33
|
+
</MyazaText>
|
|
34
|
+
<MyazaText variant="bodySmall" color={colors.textSecondary} style={{ textAlign: 'center' }}>
|
|
35
|
+
Tap Continue to submit, or Retake to try again.
|
|
36
|
+
</MyazaText>
|
|
37
|
+
</View>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Layout constants and timings shared across the liveness step's pieces.
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
export const StyleAbsFill = {
|
|
6
|
+
position: 'absolute' as const,
|
|
7
|
+
top: 0,
|
|
8
|
+
left: 0,
|
|
9
|
+
right: 0,
|
|
10
|
+
bottom: 0,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* How long to wait for the face to reappear before abandoning a capture.
|
|
15
|
+
*
|
|
16
|
+
* Generous: a full-screen colour flash can make a face momentarily
|
|
17
|
+
* undetectable, and giving up on one dropped frame would restart a sequence the
|
|
18
|
+
* user is still correctly inside.
|
|
19
|
+
*/
|
|
20
|
+
export const FRESH_FACE_TIMEOUT_MS = 1500;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Reserved height of the instruction line. Fixed so the circle does not jump as
|
|
24
|
+
* the wording changes.
|
|
25
|
+
*/
|
|
26
|
+
export const INSTRUCTION_HEIGHT = 26;
|
|
27
|
+
|
|
28
|
+
// Lighting warning palette — 1:1 with the Flutter `_LightingWarningBanner`.
|
|
29
|
+
export const AMBER_50 = '#FFFBEB';
|
|
30
|
+
export const AMBER_200 = '#FDE68A';
|
|
31
|
+
export const AMBER_800 = '#92400E';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Barrel for the liveness step's pieces, so LivenessStep imports one path.
|
|
2
|
+
export { StyleAbsFill, FRESH_FACE_TIMEOUT_MS, INSTRUCTION_HEIGHT } from './constants';
|
|
3
|
+
export { LightingBanner } from './LightingBanner';
|
|
4
|
+
export { resolveGuidance } from './resolveGuidance';
|
|
5
|
+
export { ProgressDots } from './ProgressDots';
|
|
6
|
+
export { SelfiePreview } from './SelfiePreview';
|
|
7
|
+
export { useSelfieUpload } from './useSelfieUpload';
|
|
8
|
+
export { LivenessComplete, LivenessFailed } from './LivenessOutcome';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
lightingGuidanceText,
|
|
3
|
+
positionGuidanceText,
|
|
4
|
+
type useLiveness,
|
|
5
|
+
} from '../../liveness/useLiveness';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Which single line the step shows, and in what tone.
|
|
9
|
+
//
|
|
10
|
+
// Ordered by severity: a warning the user must act on outranks the current
|
|
11
|
+
// instruction, so "move closer" is never buried under "nod your head".
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export function resolveGuidance(l: ReturnType<typeof useLiveness>): { text: string; tone: 'normal' | 'error' } {
|
|
15
|
+
if (l.multipleFaces) return { text: l.instruction, tone: 'error' };
|
|
16
|
+
// Lighting is shown in its own banner (below), not as the main instruction.
|
|
17
|
+
if (l.phase === 'positioning' && !l.faceDetected) {
|
|
18
|
+
return { text: 'Position your face in the circle', tone: 'normal' };
|
|
19
|
+
}
|
|
20
|
+
if (l.positionGuidance) return { text: positionGuidanceText(l.positionGuidance), tone: 'error' };
|
|
21
|
+
if (l.wrongGesture) return { text: 'Wrong gesture — follow the prompt', tone: 'error' };
|
|
22
|
+
return { text: l.instruction, tone: 'normal' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Numbered progress steps with connectors — mirrors the web/Flutter SDKs:
|
|
26
|
+
// passed = solid green + checkmark, active = green-outlined number, pending =
|
|
27
|
+
// grey number. `active` marks the current (completed-th) step as in-progress.
|