@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,207 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius, spacing } from '../config/theme';
|
|
5
|
+
import { useKyc, useKycConfig, useKycStore, useTheme } from '../components/runtime';
|
|
6
|
+
import { MyazaText } from '../components/Typography';
|
|
7
|
+
import { MyazaButton } from '../components/MyazaButton';
|
|
8
|
+
import { KeyPersonCard } from './KeyPersonCard';
|
|
9
|
+
import { KeyPersonSheet } from './KeyPersonSheet';
|
|
10
|
+
import { keyPeopleMinEntries } from '../config/businessSteps';
|
|
11
|
+
import {
|
|
12
|
+
emptyKeyPerson,
|
|
13
|
+
invalidKeyPersonRows,
|
|
14
|
+
isKeyPersonRowBlank,
|
|
15
|
+
isKeyPersonRowValid,
|
|
16
|
+
MAX_KEY_PEOPLE_ROWS,
|
|
17
|
+
type KeyPersonEntry,
|
|
18
|
+
} from '../config/keyPeople';
|
|
19
|
+
import type { KeyPersonRole } from '../types/business';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Applicant-declared directors, owners and signatories.
|
|
23
|
+
//
|
|
24
|
+
// The registry lookup discovers people too, and the server reconciles the two:
|
|
25
|
+
// someone the registry names who the applicant did NOT list is flagged
|
|
26
|
+
// `undisclosed`, which is a risk signal in its own right. So this screen is not
|
|
27
|
+
// merely data entry — what the applicant chooses to omit is evidence.
|
|
28
|
+
//
|
|
29
|
+
// The list stays a clean stack of compact summary cards; the FORM lives in the
|
|
30
|
+
// add/edit sheet (KeyPersonSheet). Tapping a card edits it; "Add a person"
|
|
31
|
+
// opens a fresh sheet. Removal is inside the edit sheet, behind a deliberate
|
|
32
|
+
// tap — never one stray touch on the list.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
export const businessKeyPeopleMeta = {
|
|
36
|
+
title: 'Directors & owners',
|
|
37
|
+
description:
|
|
38
|
+
"List the company's directors and owners of 25% or more. Each will receive a link to verify their identity.",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type SheetState = { mode: 'add' } | { mode: 'edit'; index: number } | null;
|
|
42
|
+
|
|
43
|
+
export function BusinessKeyPeopleStep(): React.ReactElement {
|
|
44
|
+
const config = useKycConfig();
|
|
45
|
+
const store = useKycStore();
|
|
46
|
+
const { colors } = useTheme();
|
|
47
|
+
const rows = useKyc((s) => s.businessApplication.keyPeople);
|
|
48
|
+
const registryCountry = useKyc((s) => s.business.country);
|
|
49
|
+
const [sheet, setSheet] = useState<SheetState>(null);
|
|
50
|
+
|
|
51
|
+
const minEntries = keyPeopleMinEntries(config.business);
|
|
52
|
+
const validCount = rows.filter(isKeyPersonRowValid).length;
|
|
53
|
+
const invalidRows = invalidKeyPersonRows(rows);
|
|
54
|
+
const uboThreshold = config.business?.keyPeople?.ownershipThreshold ?? 25;
|
|
55
|
+
|
|
56
|
+
// Combined ownership above 100% is factually impossible — catch the typo
|
|
57
|
+
// here rather than shipping it into the registry cross-check as a doomed
|
|
58
|
+
// mismatch. Under 100% is fine (not every owner has to be listed).
|
|
59
|
+
const pctOf = (row: KeyPersonEntry): number => {
|
|
60
|
+
const n = Number(row.ownershipPct);
|
|
61
|
+
return row.ownershipPct.trim() !== '' && Number.isFinite(n) ? n : 0;
|
|
62
|
+
};
|
|
63
|
+
const totalPct = rows.reduce((sum, row) => sum + pctOf(row), 0);
|
|
64
|
+
const overAllocated = totalPct > 100;
|
|
65
|
+
const fmtPct = (n: number): string => (Number.isInteger(n) ? String(n) : n.toFixed(1));
|
|
66
|
+
|
|
67
|
+
const canContinue = validCount >= minEntries && invalidRows.length === 0 && !overAllocated;
|
|
68
|
+
|
|
69
|
+
// New people default to the business's registry country (picked on the
|
|
70
|
+
// details step) — most directors are local, and a foreign one just switches
|
|
71
|
+
// theirs. Mirrors the web and Flutter SDKs.
|
|
72
|
+
const defaultCountry = registryCountry ?? config.business?.country ?? '';
|
|
73
|
+
|
|
74
|
+
const commit = (next: KeyPersonEntry[]): void => store.getState().setKeyPeople(next);
|
|
75
|
+
const handleSave = (entry: KeyPersonEntry): void => {
|
|
76
|
+
if (sheet?.mode === 'edit') {
|
|
77
|
+
commit(rows.map((row, i) => (i === sheet.index ? entry : row)));
|
|
78
|
+
} else {
|
|
79
|
+
commit([...rows, entry]);
|
|
80
|
+
}
|
|
81
|
+
setSheet(null);
|
|
82
|
+
};
|
|
83
|
+
const handleRemove = (): void => {
|
|
84
|
+
if (sheet?.mode === 'edit') commit(rows.filter((_, i) => i !== sheet.index));
|
|
85
|
+
setSheet(null);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Indexed access may be undefined under strict indexing — resolved once here
|
|
89
|
+
// so the sheet only ever mounts with a real entry.
|
|
90
|
+
const editEntry = sheet?.mode === 'edit' ? rows[sheet.index] : undefined;
|
|
91
|
+
|
|
92
|
+
const handleContinue = (): void => {
|
|
93
|
+
if (!canContinue) return;
|
|
94
|
+
// Half-typed rows are dropped rather than submitted: an entry the user
|
|
95
|
+
// abandoned is not a person they disclosed.
|
|
96
|
+
commit(rows.filter(isKeyPersonRowValid));
|
|
97
|
+
store.getState().nextStep();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// The dashed hint boxes and their copy mirror the web SDK's step 1:1.
|
|
101
|
+
const hint =
|
|
102
|
+
rows.length === 0 && minEntries === 0
|
|
103
|
+
? "You can skip this if you're unsure — we'll identify directors and owners from the official registry. Adding them here speeds up the review."
|
|
104
|
+
: minEntries > 0 && validCount < minEntries
|
|
105
|
+
? `List at least ${minEntries} ${minEntries === 1 ? 'person' : 'people'} to continue${
|
|
106
|
+
validCount > 0 ? ` (${validCount} of ${minEntries} added)` : ''
|
|
107
|
+
}.`
|
|
108
|
+
: null;
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<View>
|
|
112
|
+
{hint ? (
|
|
113
|
+
<View
|
|
114
|
+
style={{
|
|
115
|
+
borderWidth: 1,
|
|
116
|
+
borderStyle: 'dashed',
|
|
117
|
+
borderColor: colors.border,
|
|
118
|
+
borderRadius: radius.sm,
|
|
119
|
+
backgroundColor: colors.backgroundSecondary,
|
|
120
|
+
padding: spacing.md,
|
|
121
|
+
marginBottom: spacing.md,
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<MyazaText variant="bodySmall" color={colors.textSecondary}>
|
|
125
|
+
{hint}
|
|
126
|
+
</MyazaText>
|
|
127
|
+
</View>
|
|
128
|
+
) : null}
|
|
129
|
+
|
|
130
|
+
{rows.map((row, index) =>
|
|
131
|
+
isKeyPersonRowBlank(row) ? null : (
|
|
132
|
+
<KeyPersonCard key={index} entry={row} onPress={() => setSheet({ mode: 'edit', index })} />
|
|
133
|
+
),
|
|
134
|
+
)}
|
|
135
|
+
{/* The cards and the add affordance are different things — give the
|
|
136
|
+
boundary some air (cards already carry a small bottom margin). */}
|
|
137
|
+
{rows.some((row) => !isKeyPersonRowBlank(row)) ? (
|
|
138
|
+
<View style={{ height: spacing.sm }} />
|
|
139
|
+
) : null}
|
|
140
|
+
|
|
141
|
+
{rows.length < MAX_KEY_PEOPLE_ROWS ? (
|
|
142
|
+
<MyazaButton
|
|
143
|
+
label="Add a person"
|
|
144
|
+
variant="outline"
|
|
145
|
+
leadingIcon="user-plus"
|
|
146
|
+
onPress={() => setSheet({ mode: 'add' })}
|
|
147
|
+
/>
|
|
148
|
+
) : (
|
|
149
|
+
<MyazaText variant="bodySmall" color={colors.textMuted} style={{ textAlign: 'center' }}>
|
|
150
|
+
{`You can list up to ${MAX_KEY_PEOPLE_ROWS} people here.`}
|
|
151
|
+
</MyazaText>
|
|
152
|
+
)}
|
|
153
|
+
{/* Breathing room between the add-person affordance and the summary bar —
|
|
154
|
+
matches the Flutter screen's SizedBox after its outline button. */}
|
|
155
|
+
<View style={{ height: spacing.md }} />
|
|
156
|
+
|
|
157
|
+
{/* Total-ownership summary at the DECISION point — the disabled Continue
|
|
158
|
+
button always explains itself, wherever the offending card is. */}
|
|
159
|
+
{totalPct > 0 ? (
|
|
160
|
+
<View
|
|
161
|
+
style={{
|
|
162
|
+
flexDirection: 'row',
|
|
163
|
+
justifyContent: 'space-between',
|
|
164
|
+
alignItems: 'center',
|
|
165
|
+
backgroundColor: overAllocated ? colors.errorBg : colors.backgroundSecondary,
|
|
166
|
+
borderRadius: radius.sm,
|
|
167
|
+
paddingHorizontal: spacing.md,
|
|
168
|
+
paddingVertical: spacing.sm + 4,
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
<MyazaText variant="bodySmall" color={overAllocated ? colors.error : colors.textMuted}>
|
|
172
|
+
Total ownership listed
|
|
173
|
+
</MyazaText>
|
|
174
|
+
<MyazaText
|
|
175
|
+
variant="bodySmall"
|
|
176
|
+
color={overAllocated ? colors.error : colors.textDark}
|
|
177
|
+
style={{ fontWeight: '700' }}
|
|
178
|
+
>
|
|
179
|
+
{`${fmtPct(totalPct)}%`}
|
|
180
|
+
</MyazaText>
|
|
181
|
+
</View>
|
|
182
|
+
) : null}
|
|
183
|
+
{overAllocated ? (
|
|
184
|
+
<MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.xs }}>
|
|
185
|
+
{`Together the percentages can't exceed 100% — reduce them by ${fmtPct(totalPct - 100)}%.`}
|
|
186
|
+
</MyazaText>
|
|
187
|
+
) : null}
|
|
188
|
+
|
|
189
|
+
<View style={{ height: spacing.md }} />
|
|
190
|
+
<MyazaButton label="Continue" onPress={handleContinue} disabled={!canContinue} />
|
|
191
|
+
|
|
192
|
+
{sheet && (sheet.mode === 'add' || editEntry) ? (
|
|
193
|
+
<KeyPersonSheet
|
|
194
|
+
mode={sheet.mode}
|
|
195
|
+
initial={editEntry ?? { ...emptyKeyPerson(), country: defaultCountry }}
|
|
196
|
+
uboThreshold={uboThreshold}
|
|
197
|
+
otherPctTotal={editEntry ? totalPct - pctOf(editEntry) : totalPct}
|
|
198
|
+
onSave={handleSave}
|
|
199
|
+
onRemove={sheet.mode === 'edit' ? handleRemove : undefined}
|
|
200
|
+
onClose={() => setSheet(null)}
|
|
201
|
+
/>
|
|
202
|
+
) : null}
|
|
203
|
+
</View>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export type { KeyPersonRole };
|
|
@@ -0,0 +1,83 @@
|
|
|
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 { MyazaInput } from '../components/MyazaInput';
|
|
8
|
+
import type { CompanyInfoField, CompanyInfoMode } from '../types/business';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// The company profile a KYB workflow asks for.
|
|
12
|
+
//
|
|
13
|
+
// Split from BusinessDetailsStep (200-line rule). Each field's mode comes from
|
|
14
|
+
// the workflow and must match the server's resolution exactly — a field marked
|
|
15
|
+
// optional here but required there produces a 422 the user cannot act on.
|
|
16
|
+
// Section header, captions and placeholders mirror the web SDK's
|
|
17
|
+
// BusinessCompanyInfoFields 1:1.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export const COMPANY_FIELD_LABELS: Record<CompanyInfoField, string> = {
|
|
21
|
+
address: 'Registered address',
|
|
22
|
+
email: 'Business email',
|
|
23
|
+
phone: 'Business phone',
|
|
24
|
+
website: 'Website',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const COMPANY_FIELD_PLACEHOLDERS: Record<CompanyInfoField, string> = {
|
|
28
|
+
address: 'e.g. 12 Marina Road, Lagos',
|
|
29
|
+
email: 'hello@company.com',
|
|
30
|
+
phone: '+234 800 000 0000',
|
|
31
|
+
website: 'company.com',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function CompanyInfoFields({
|
|
35
|
+
fields,
|
|
36
|
+
modes,
|
|
37
|
+
values,
|
|
38
|
+
missing,
|
|
39
|
+
showErrors,
|
|
40
|
+
onChange,
|
|
41
|
+
}: {
|
|
42
|
+
fields: CompanyInfoField[];
|
|
43
|
+
modes: Record<CompanyInfoField, CompanyInfoMode>;
|
|
44
|
+
values: Record<CompanyInfoField, string>;
|
|
45
|
+
missing: CompanyInfoField[];
|
|
46
|
+
showErrors: boolean;
|
|
47
|
+
onChange: (field: CompanyInfoField, value: string) => void;
|
|
48
|
+
}): React.ReactElement | null {
|
|
49
|
+
const { colors } = useTheme();
|
|
50
|
+
if (fields.length === 0) return null;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<>
|
|
54
|
+
<View style={{ height: spacing.md }} />
|
|
55
|
+
<MyazaText variant="bodyMedium" style={{ fontWeight: '600' }}>
|
|
56
|
+
Company information
|
|
57
|
+
</MyazaText>
|
|
58
|
+
<MyazaText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: 2 }}>
|
|
59
|
+
We verify these details against the official registry record.
|
|
60
|
+
</MyazaText>
|
|
61
|
+
{fields.map((field) => (
|
|
62
|
+
<View key={field}>
|
|
63
|
+
<View style={{ height: spacing.sm }} />
|
|
64
|
+
<MyazaInput
|
|
65
|
+
label={`${COMPANY_FIELD_LABELS[field]}${modes[field] === 'required' ? ' *' : ' (optional)'}`}
|
|
66
|
+
value={values[field]}
|
|
67
|
+
onChangeText={(text) => onChange(field, text)}
|
|
68
|
+
placeholder={COMPANY_FIELD_PLACEHOLDERS[field]}
|
|
69
|
+
keyboardType={
|
|
70
|
+
field === 'email' ? 'email-address' : field === 'phone' ? 'phone-pad' : 'default'
|
|
71
|
+
}
|
|
72
|
+
autoCapitalize="none"
|
|
73
|
+
error={
|
|
74
|
+
showErrors && missing.includes(field)
|
|
75
|
+
? `${COMPANY_FIELD_LABELS[field]} is required.`
|
|
76
|
+
: null
|
|
77
|
+
}
|
|
78
|
+
/>
|
|
79
|
+
</View>
|
|
80
|
+
))}
|
|
81
|
+
</>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -1,52 +1,31 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import { Pressable, View } from 'react-native';
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Linking, Pressable, View } from 'react-native';
|
|
3
|
+
import Svg, { Circle, Defs, LinearGradient, Stop } from 'react-native-svg';
|
|
3
4
|
|
|
4
5
|
import { radius, spacing } from '../config/theme';
|
|
6
|
+
import { PRIVACY_URL, TERMS_URL } from '../config/brand';
|
|
5
7
|
import { useKyc, useKycConfig, useTheme } from '../components/runtime';
|
|
6
8
|
import { MyazaText } from '../components/Typography';
|
|
7
9
|
import { MyazaButton } from '../components/MyazaButton';
|
|
8
|
-
import { Icon
|
|
9
|
-
import {
|
|
10
|
+
import { Icon } from '../components/Icon';
|
|
11
|
+
import { buildConsentModel } from './consent/model';
|
|
10
12
|
|
|
11
|
-
// Consent / welcome screen — 1:1 with the
|
|
12
|
-
// shield hero, token-filled greeting, a "DURING
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
interface ProcessStep {
|
|
20
|
-
icon: IconName;
|
|
21
|
-
label: string;
|
|
22
|
-
}
|
|
13
|
+
// Consent / welcome screen — 1:1 with the web SDK's ConsentStep and the
|
|
14
|
+
// Flutter SDK's ConsentScreen: shield hero, token-filled greeting, a "DURING
|
|
15
|
+
// THIS PROCESS WE WILL" card, the derived legal notice, Continue, lock footer.
|
|
16
|
+
// Everything the screen SAYS — title, description, step list, the biometric
|
|
17
|
+
// sentence — is derived in consent/model.ts from what the flow actually does,
|
|
18
|
+
// so a KYB flow gets the business copy and never claims an ID/selfie step it
|
|
19
|
+
// doesn't run. The header title is intentionally empty for this step; the
|
|
20
|
+
// screen owns its hero.
|
|
23
21
|
|
|
24
22
|
export function ConsentStep(): React.ReactElement {
|
|
25
23
|
const { colors } = useTheme();
|
|
26
24
|
const config = useKycConfig();
|
|
27
25
|
const nextStep = useKyc((s) => s.nextStep);
|
|
28
|
-
const [agreed, setAgreed] = useState(false);
|
|
29
|
-
|
|
30
|
-
const firstName = config.userData?.firstName ?? '';
|
|
31
|
-
const title = config.consent?.title
|
|
32
|
-
? fillTokens(config.consent.title, config.userData)
|
|
33
|
-
: firstName
|
|
34
|
-
? `Welcome, ${firstName}`
|
|
35
|
-
: 'Identity Verification';
|
|
36
|
-
const description = config.consent?.description
|
|
37
|
-
? fillTokens(config.consent.description, config.userData)
|
|
38
|
-
: DEFAULT_DESCRIPTION;
|
|
39
26
|
|
|
40
|
-
const steps
|
|
41
|
-
|
|
42
|
-
{ icon: 'user', label: 'Collect basic personal information' },
|
|
43
|
-
...(config.enableDocumentCapture
|
|
44
|
-
? [{ icon: 'scan-line' as IconName, label: 'Capture a photo of your ID document' }]
|
|
45
|
-
: []),
|
|
46
|
-
...(config.enableSelfie
|
|
47
|
-
? [{ icon: 'scan-face' as IconName, label: 'Take a selfie for facial verification' }]
|
|
48
|
-
: []),
|
|
49
|
-
];
|
|
27
|
+
const { isBusiness, title, description, steps, capturesFace, recordsVideo } =
|
|
28
|
+
buildConsentModel(config);
|
|
50
29
|
|
|
51
30
|
return (
|
|
52
31
|
<View>
|
|
@@ -62,7 +41,6 @@ export function ConsentStep(): React.ReactElement {
|
|
|
62
41
|
width: 56,
|
|
63
42
|
height: 56,
|
|
64
43
|
borderRadius: 28,
|
|
65
|
-
backgroundColor: colors.primary,
|
|
66
44
|
alignItems: 'center',
|
|
67
45
|
justifyContent: 'center',
|
|
68
46
|
shadowColor: colors.primary,
|
|
@@ -72,6 +50,22 @@ export function ConsentStep(): React.ReactElement {
|
|
|
72
50
|
elevation: 6,
|
|
73
51
|
}}
|
|
74
52
|
>
|
|
53
|
+
{/* Top-left → bottom-right gradient badge, matching Flutter. A flat
|
|
54
|
+
fill was the last visual difference on this screen. The second
|
|
55
|
+
stop is the SAME primary at 70% opacity rather than a
|
|
56
|
+
pre-blended colour: composited over the sheet it produces
|
|
57
|
+
Flutter's `alphaBlend(primary@0.7, background)` exactly, and it
|
|
58
|
+
keeps working when the consumer overrides primaryColor or the
|
|
59
|
+
theme flips, which a hardcoded blend would not. */}
|
|
60
|
+
<Svg width={56} height={56} style={{ position: 'absolute' }}>
|
|
61
|
+
<Defs>
|
|
62
|
+
<LinearGradient id="myazaShieldHero" x1="0" y1="0" x2="1" y2="1">
|
|
63
|
+
<Stop offset="0" stopColor={colors.primary} stopOpacity={1} />
|
|
64
|
+
<Stop offset="1" stopColor={colors.primary} stopOpacity={0.7} />
|
|
65
|
+
</LinearGradient>
|
|
66
|
+
</Defs>
|
|
67
|
+
<Circle cx={28} cy={28} r={28} fill="url(#myazaShieldHero)" />
|
|
68
|
+
</Svg>
|
|
75
69
|
<Icon name="shield" size={28} color={colors.onPrimary} />
|
|
76
70
|
</View>
|
|
77
71
|
</View>
|
|
@@ -118,46 +112,52 @@ export function ConsentStep(): React.ReactElement {
|
|
|
118
112
|
<View style={{ height: spacing.lg }} />
|
|
119
113
|
|
|
120
114
|
{/* Consent checkbox */}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
width: 22,
|
|
134
|
-
height: 22,
|
|
135
|
-
borderRadius: radius.xs - 2,
|
|
136
|
-
backgroundColor: agreed ? colors.primary : 'transparent',
|
|
137
|
-
borderWidth: 1.5,
|
|
138
|
-
borderColor: agreed ? colors.primary : colors.gray400,
|
|
139
|
-
alignItems: 'center',
|
|
140
|
-
justifyContent: 'center',
|
|
141
|
-
marginTop: 1,
|
|
142
|
-
}}
|
|
115
|
+
{/* Consent is given by ACTING now, so the notice sits immediately above
|
|
116
|
+
the button it describes — adjacency is what makes it informed.
|
|
117
|
+
The biometric sentence is DERIVED: claiming facial recognition on a
|
|
118
|
+
flow with no selfie step would be false, and recording video without
|
|
119
|
+
saying so is the failure that actually matters. */}
|
|
120
|
+
<MyazaText variant="bodySmall" style={{ lineHeight: 18 }}>
|
|
121
|
+
By tapping Continue, you agree to the{' '}
|
|
122
|
+
<MyazaText
|
|
123
|
+
variant="bodySmall"
|
|
124
|
+
color={colors.textDark}
|
|
125
|
+
style={{ fontWeight: '600', textDecorationLine: 'underline' }}
|
|
126
|
+
onPress={() => void Linking.openURL(TERMS_URL).catch(() => undefined)}
|
|
143
127
|
>
|
|
144
|
-
|
|
145
|
-
</
|
|
146
|
-
|
|
147
|
-
<MyazaText
|
|
148
|
-
|
|
128
|
+
End User Terms
|
|
129
|
+
</MyazaText>{' '}
|
|
130
|
+
and{' '}
|
|
131
|
+
<MyazaText
|
|
132
|
+
variant="bodySmall"
|
|
133
|
+
color={colors.textDark}
|
|
134
|
+
style={{ fontWeight: '600', textDecorationLine: 'underline' }}
|
|
135
|
+
onPress={() => void Linking.openURL(PRIVACY_URL).catch(() => undefined)}
|
|
136
|
+
>
|
|
137
|
+
Privacy Policy
|
|
149
138
|
</MyazaText>
|
|
150
|
-
|
|
139
|
+
, and consent to your {isBusiness ? 'business and personal data' : 'personal data'} being
|
|
140
|
+
processed to verify your identity.
|
|
141
|
+
{capturesFace
|
|
142
|
+
? ' This includes facial recognition and recording this session.'
|
|
143
|
+
: recordsVideo
|
|
144
|
+
? ' This includes recording this session.'
|
|
145
|
+
: ''}
|
|
146
|
+
</MyazaText>
|
|
151
147
|
|
|
152
148
|
<View style={{ height: spacing.lg }} />
|
|
153
|
-
<MyazaButton label="Continue" onPress={
|
|
149
|
+
<MyazaButton label="Continue" onPress={nextStep} />
|
|
154
150
|
<View style={{ height: spacing.sm }} />
|
|
155
151
|
|
|
156
152
|
{/* Footer */}
|
|
157
153
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }}>
|
|
158
154
|
<Icon name="lock" size={13} color={colors.textMuted} />
|
|
159
155
|
<View style={{ width: 6 }} />
|
|
160
|
-
|
|
156
|
+
{/* flexShrink so a wide brand font wraps this instead of clipping it —
|
|
157
|
+
a Text in a row without it overflows rather than wrapping. */}
|
|
158
|
+
<MyazaText variant="bodySmall" style={{ flexShrink: 1 }}>
|
|
159
|
+
Your data is encrypted and securely processed
|
|
160
|
+
</MyazaText>
|
|
161
161
|
</View>
|
|
162
162
|
</View>
|
|
163
163
|
);
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, 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
|
+
import { WhatsAppIcon } from '../components/WhatsAppIcon';
|
|
9
|
+
import { CHANNEL_LABELS, type PhoneOtpChannel } from '../config/contact';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// How the user wants their one-time code delivered.
|
|
13
|
+
//
|
|
14
|
+
// The org decides which channels are ON OFFER; the person receiving the code
|
|
15
|
+
// decides between them, because only they know whether they have WhatsApp
|
|
16
|
+
// installed or whether SMS is landing for them today. With a single offered
|
|
17
|
+
// channel there is no choice to make and this renders nothing.
|
|
18
|
+
//
|
|
19
|
+
// Card shape, icon and selected treatment mirror the web SDK's picker so the
|
|
20
|
+
// step reads the same on every platform.
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const HINTS: Record<PhoneOtpChannel, string> = {
|
|
24
|
+
sms: 'Text message',
|
|
25
|
+
whatsapp: 'Needs WhatsApp',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** WhatsApp needs its own brand mark; SMS reads fine as a generic glyph. */
|
|
29
|
+
function ChannelGlyph({
|
|
30
|
+
channel,
|
|
31
|
+
color,
|
|
32
|
+
}: {
|
|
33
|
+
channel: PhoneOtpChannel;
|
|
34
|
+
color: string;
|
|
35
|
+
}): React.ReactElement {
|
|
36
|
+
return channel === 'whatsapp' ? (
|
|
37
|
+
<WhatsAppIcon size={16} color={color} />
|
|
38
|
+
) : (
|
|
39
|
+
<Icon name="message-square" size={16} color={color} />
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function ContactChannelChoice({
|
|
44
|
+
offered,
|
|
45
|
+
picked,
|
|
46
|
+
disabled,
|
|
47
|
+
onPick,
|
|
48
|
+
}: {
|
|
49
|
+
offered: PhoneOtpChannel[];
|
|
50
|
+
picked: PhoneOtpChannel;
|
|
51
|
+
disabled?: boolean;
|
|
52
|
+
onPick: (channel: PhoneOtpChannel) => void;
|
|
53
|
+
}): React.ReactElement | null {
|
|
54
|
+
const { colors } = useTheme();
|
|
55
|
+
if (offered.length < 2) return null;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<View>
|
|
59
|
+
<View style={{ height: spacing.md }} />
|
|
60
|
+
<MyazaText variant="bodySmall" color={colors.textMuted}>
|
|
61
|
+
How should we send it?
|
|
62
|
+
</MyazaText>
|
|
63
|
+
<View style={{ height: spacing.xs }} />
|
|
64
|
+
<View style={{ flexDirection: 'row', gap: spacing.sm }}>
|
|
65
|
+
{offered.map((channel) => (
|
|
66
|
+
<ChannelTile
|
|
67
|
+
key={channel}
|
|
68
|
+
channel={channel}
|
|
69
|
+
selected={picked === channel}
|
|
70
|
+
disabled={disabled}
|
|
71
|
+
onPress={() => onPick(channel)}
|
|
72
|
+
/>
|
|
73
|
+
))}
|
|
74
|
+
</View>
|
|
75
|
+
</View>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ChannelTile({
|
|
80
|
+
channel,
|
|
81
|
+
selected,
|
|
82
|
+
disabled,
|
|
83
|
+
onPress,
|
|
84
|
+
}: {
|
|
85
|
+
channel: PhoneOtpChannel;
|
|
86
|
+
selected: boolean;
|
|
87
|
+
disabled?: boolean;
|
|
88
|
+
onPress: () => void;
|
|
89
|
+
}): React.ReactElement {
|
|
90
|
+
const { colors } = useTheme();
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<Pressable
|
|
94
|
+
onPress={disabled ? undefined : onPress}
|
|
95
|
+
accessibilityRole="radio"
|
|
96
|
+
accessibilityState={{ checked: selected, disabled }}
|
|
97
|
+
accessibilityLabel={CHANNEL_LABELS[channel]}
|
|
98
|
+
style={{
|
|
99
|
+
flex: 1,
|
|
100
|
+
flexDirection: 'row',
|
|
101
|
+
alignItems: 'center',
|
|
102
|
+
gap: spacing.sm,
|
|
103
|
+
paddingHorizontal: spacing.sm,
|
|
104
|
+
paddingVertical: spacing.sm + 2,
|
|
105
|
+
borderRadius: radius.md,
|
|
106
|
+
borderWidth: 1.5,
|
|
107
|
+
borderColor: selected ? colors.primary : colors.border,
|
|
108
|
+
backgroundColor: selected ? `${colors.primary}0D` : 'transparent',
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<View
|
|
112
|
+
style={{
|
|
113
|
+
width: 34,
|
|
114
|
+
height: 34,
|
|
115
|
+
borderRadius: radius.sm,
|
|
116
|
+
alignItems: 'center',
|
|
117
|
+
justifyContent: 'center',
|
|
118
|
+
backgroundColor: selected ? `${colors.primary}1A` : colors.backgroundSecondary,
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
<ChannelGlyph channel={channel} color={selected ? colors.primary : colors.textMuted} />
|
|
122
|
+
</View>
|
|
123
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
124
|
+
<MyazaText variant="bodyMedium" style={{ fontWeight: "600" }}>
|
|
125
|
+
{CHANNEL_LABELS[channel]}
|
|
126
|
+
</MyazaText>
|
|
127
|
+
<MyazaText variant="bodySmall" color={colors.textMuted} numberOfLines={1}>
|
|
128
|
+
{HINTS[channel]}
|
|
129
|
+
</MyazaText>
|
|
130
|
+
</View>
|
|
131
|
+
</Pressable>
|
|
132
|
+
);
|
|
133
|
+
}
|