@myazahq/kyc-sdk-react-native 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -8
- package/android/build.gradle +6 -0
- package/android/src/main/AndroidManifest.xml +14 -4
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaEmrtd.kt +358 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +88 -1
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaRectDetector.kt +34 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +118 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/Jp2Decoder.kt +117 -0
- package/app.plugin.js +102 -13
- package/ios/HybridMyazaEmrtd.swift +447 -0
- package/ios/HybridMyazaFaceDetector.swift +104 -2
- package/ios/HybridMyazaRectDetector.swift +103 -0
- package/ios/HybridMyazaTextRecognizer.swift +111 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/KycSdkReactNative+autolinking.cmake +87 -0
- package/nitrogen/generated/android/KycSdkReactNative+autolinking.gradle +27 -0
- package/nitrogen/generated/android/KycSdkReactNativeOnLoad.cpp +102 -0
- package/nitrogen/generated/android/KycSdkReactNativeOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JDetectedRect.hpp +81 -0
- package/nitrogen/generated/android/c++/JEmrtdApduResponse.hpp +61 -0
- package/nitrogen/generated/android/c++/JEmrtdTagInfo.hpp +61 -0
- package/nitrogen/generated/android/c++/JFaceResult.hpp +113 -0
- package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.cpp +176 -0
- package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +60 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +63 -0
- package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.cpp +60 -0
- package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.hpp +63 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +80 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +64 -0
- package/nitrogen/generated/android/c++/JTextResult.hpp +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/DetectedRect.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdApduResponse.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdTagInfo.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/FaceResult.kt +121 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaEmrtdSpec.kt +103 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +55 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaRectDetectorSpec.kt +55 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +60 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/KycSdkReactNativeOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/TextResult.kt +51 -0
- package/nitrogen/generated/ios/KycSdkReactNative+autolinking.rb +62 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +143 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +423 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Umbrella.hpp +81 -0
- package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.mm +57 -0
- package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.swift +62 -0
- package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.hpp +184 -0
- package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +87 -0
- package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.hpp +87 -0
- package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +98 -0
- package/nitrogen/generated/ios/swift/DetectedRect.swift +59 -0
- package/nitrogen/generated/ios/swift/EmrtdApduResponse.swift +34 -0
- package/nitrogen/generated/ios/swift/EmrtdTagInfo.swift +34 -0
- package/nitrogen/generated/ios/swift/FaceResult.swift +99 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_EmrtdApduResponse.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_EmrtdTagInfo.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_TextResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec.swift +67 -0
- package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec_cxx.swift +317 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +143 -0
- package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec_cxx.swift +143 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +57 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +162 -0
- package/nitrogen/generated/ios/swift/TextResult.swift +35 -0
- package/nitrogen/generated/shared/c++/DetectedRect.hpp +107 -0
- package/nitrogen/generated/shared/c++/EmrtdApduResponse.hpp +87 -0
- package/nitrogen/generated/shared/c++/EmrtdTagInfo.hpp +87 -0
- package/nitrogen/generated/shared/c++/FaceResult.hpp +139 -0
- package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.cpp +33 -0
- package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.hpp +80 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +67 -0
- package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.hpp +67 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +70 -0
- package/nitrogen/generated/shared/c++/TextResult.hpp +84 -0
- package/package.json +16 -4
- package/src/MyazaKYC.tsx +115 -22
- package/src/capture/documentIdentity.ts +153 -0
- package/src/capture/documentSignals.ts +177 -0
- package/src/capture/documentTextGate.ts +171 -0
- package/src/capture/hints.ts +57 -0
- package/src/capture/index.ts +24 -0
- package/src/capture/rectDetector.ts +84 -0
- package/src/capture/rectGate.ts +91 -0
- package/src/capture/useAutoCapture.ts +198 -0
- package/src/components/CameraViewfinder.tsx +226 -70
- package/src/components/ContactCodeEntry.tsx +139 -0
- package/src/components/CountryField.tsx +87 -0
- package/src/components/CountryFlag.tsx +15 -15
- package/src/components/DialCodePicker.tsx +186 -0
- package/src/components/DocumentCropper.tsx +1 -1
- package/src/components/DocumentReview.tsx +142 -0
- package/src/components/DocumentReviewSide.tsx +135 -0
- package/src/components/DocumentReviewZoom.tsx +119 -0
- package/src/components/ExpiryCountdown.tsx +52 -0
- package/src/components/FlashOverlay.tsx +156 -0
- package/src/components/Icon.tsx +67 -1
- package/src/components/KycFlow.tsx +66 -94
- package/src/components/KycSheet.tsx +148 -18
- package/src/components/MediaSourceSheet.tsx +163 -0
- package/src/components/MyazaButton.tsx +14 -2
- package/src/components/MyazaDateField.tsx +240 -0
- package/src/components/MyazaInput.tsx +73 -26
- package/src/components/MyazaSelect.tsx +283 -0
- package/src/components/MyazaWordmark.tsx +33 -0
- package/src/components/OptionRow.tsx +132 -0
- package/src/components/PhoneNumberInput.tsx +149 -0
- package/src/components/PoweredBy.tsx +89 -0
- package/src/components/ReadyPrimer.tsx +149 -0
- package/src/components/StepView.tsx +91 -0
- package/src/components/Typography.tsx +18 -3
- package/src/components/VerifiedNotice.tsx +38 -0
- package/src/components/WhatsAppIcon.tsx +29 -0
- package/src/components/WorkflowGate.tsx +128 -0
- package/src/components/brand-font.ts +113 -0
- package/src/components/flashHoleGeometry.ts +35 -0
- package/src/components/flow/FatalConfigError.tsx +56 -0
- package/src/components/fonts.ts +2 -19
- package/src/components/readyPrimerContent.ts +52 -0
- package/src/components/runtime.tsx +38 -43
- package/src/components/stepHeaderMeta.tsx +87 -0
- package/src/components/theme-provider.tsx +89 -0
- package/src/components/useWorkflowMount.ts +116 -0
- package/src/components/viewfinder/CardGuide.tsx +60 -0
- package/src/components/viewfinder/DocumentGhost.tsx +149 -0
- package/src/components/viewfinder/ImmersiveBottomBar.tsx +172 -0
- package/src/components/viewfinder/ImmersiveControls.tsx +127 -0
- package/src/components/viewfinder/ImmersiveGuide.tsx +174 -0
- package/src/components/viewfinder/ImmersiveOverlay.tsx +79 -0
- package/src/components/viewfinder/ViewfinderControls.tsx +154 -0
- package/src/config/brand.ts +97 -0
- package/src/config/business.ts +179 -0
- package/src/config/businessSteps.ts +105 -0
- package/src/config/contact.ts +159 -0
- package/src/config/countryNames.g.ts +254 -0
- package/src/config/currencyFlags.ts +144 -0
- package/src/config/font-resolve.ts +78 -0
- package/src/config/idTypes.ts +96 -1
- package/src/config/keyPeople.ts +152 -0
- package/src/config/phone.ts +39 -0
- package/src/config/proofOfAddress.ts +75 -0
- package/src/config/questionnaire.ts +153 -0
- package/src/config/regions.ts +118 -0
- package/src/config/registrationHint.ts +87 -0
- package/src/config/stepOrder.ts +118 -0
- package/src/config/theme.ts +38 -2
- package/src/config/workflowMerge.ts +124 -0
- package/src/emrtd/bac.ts +128 -0
- package/src/emrtd/bytes.ts +127 -0
- package/src/emrtd/crypto.ts +163 -0
- package/src/emrtd/der.ts +192 -0
- package/src/emrtd/dg2.ts +111 -0
- package/src/emrtd/files.ts +215 -0
- package/src/emrtd/index.ts +30 -0
- package/src/emrtd/mrzKey.ts +54 -0
- package/src/emrtd/native.ts +79 -0
- package/src/emrtd/optionalRead.ts +42 -0
- package/src/emrtd/read.ts +224 -0
- package/src/emrtd/retry.ts +98 -0
- package/src/emrtd/secureMessaging.ts +178 -0
- package/src/emrtd/session.ts +200 -0
- package/src/emrtd/stages.ts +122 -0
- package/src/index.ts +110 -0
- package/src/lib/calendar.ts +71 -0
- package/src/lib/use-keyboard-inset.ts +34 -0
- package/src/liveness/challengeManager.ts +27 -1
- package/src/liveness/faceContinuity.ts +153 -0
- package/src/liveness/flashDetector.ts +145 -0
- package/src/liveness/flashReadyGate.ts +106 -0
- package/src/liveness/flashRunner.ts +118 -0
- package/src/liveness/integritySignals.ts +60 -0
- package/src/liveness/livenessState.ts +139 -0
- package/src/liveness/speech.ts +19 -1
- package/src/liveness/types.ts +48 -1
- package/src/liveness/useFlashPhase.ts +78 -0
- package/src/liveness/useLiveness.ts +260 -75
- package/src/liveness/visionCameraFaceDetector.ts +14 -0
- package/src/mrz/extract.ts +101 -0
- package/src/mrz/parse.ts +178 -0
- package/src/mrz/textRecognizer.ts +149 -0
- package/src/screens/ApplicantRoleStep.tsx +331 -0
- package/src/screens/BusinessDetailsStep.tsx +212 -0
- package/src/screens/BusinessDocumentSlot.tsx +167 -0
- package/src/screens/BusinessDocumentsStep.tsx +213 -0
- package/src/screens/BusinessKeyPeopleStep.tsx +151 -0
- package/src/screens/BusinessKeyPersonRow.tsx +170 -0
- package/src/screens/CompanyInfoFields.tsx +83 -0
- package/src/screens/ConsentStep.tsx +68 -68
- package/src/screens/ContactChannelChoice.tsx +133 -0
- package/src/screens/ContactCodeStep.tsx +130 -0
- package/src/screens/ContactDestinationField.tsx +70 -0
- package/src/screens/ContactFooterNote.tsx +34 -0
- package/src/screens/ContactVerificationStep.tsx +199 -0
- package/src/screens/ContactVerifiedPanel.tsx +36 -0
- package/src/screens/CountrySelectStep.tsx +210 -0
- package/src/screens/DocumentCaptureStep.tsx +156 -132
- package/src/screens/IdInputStep.tsx +13 -5
- package/src/screens/IdTypeStep.tsx +74 -33
- package/src/screens/KeyPeopleInviteLinks.tsx +239 -0
- package/src/screens/LivenessStep.tsx +155 -247
- package/src/screens/MrzScanView.tsx +173 -0
- package/src/screens/NfcStep.tsx +290 -0
- package/src/screens/ProofOfAddressStep.tsx +238 -0
- package/src/screens/QuestionnaireField.tsx +219 -0
- package/src/screens/QuestionnaireMoneyField.tsx +134 -0
- package/src/screens/QuestionnaireStep.tsx +76 -0
- package/src/screens/SubmittedStep.tsx +5 -0
- package/src/screens/consent/model.ts +109 -0
- package/src/screens/document/CameraPhase.tsx +150 -0
- package/src/screens/document/RequiredPill.tsx +60 -0
- package/src/screens/liveness/LightingBanner.tsx +50 -0
- package/src/screens/liveness/LivenessOutcome.tsx +109 -0
- package/src/screens/liveness/ProgressDots.tsx +67 -0
- package/src/screens/liveness/SelfiePreview.tsx +39 -0
- package/src/screens/liveness/constants.ts +31 -0
- package/src/screens/liveness/index.ts +8 -0
- package/src/screens/liveness/resolveGuidance.ts +27 -0
- package/src/screens/liveness/useSelfieUpload.ts +107 -0
- package/src/screens/nfc/NfcMrzPrompt.tsx +40 -0
- package/src/screens/nfc/NfcReadBody.tsx +63 -0
- package/src/screens/nfc/NfcReadProgress.tsx +96 -0
- package/src/screens/nfc/NfcScanIllustration.tsx +244 -0
- package/src/screens/nfc/NfcScannedSummary.tsx +74 -0
- package/src/screens/nfc/NfcSuccessPanel.tsx +162 -0
- package/src/screens/nfc/availability.ts +73 -0
- package/src/screens/nfc/readErrorMessage.ts +93 -0
- package/src/screens/nfc/useChipPortrait.ts +58 -0
- package/src/screens/nfc/useNfcAvailability.ts +82 -0
- package/src/screens/useFlashHole.ts +105 -0
- package/src/screens/useFlashSequence.ts +96 -0
- package/src/screens/usePoaAttach.ts +121 -0
- package/src/services/api-types.ts +164 -0
- package/src/services/api-verify-types.ts +99 -0
- package/src/services/api.ts +72 -102
- package/src/services/cardCrop.ts +25 -10
- package/src/services/contactErrors.ts +43 -0
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/documentPicker.ts +20 -0
- package/src/services/fingerprint-sources.ts +129 -0
- package/src/services/fingerprint.ts +101 -0
- package/src/services/haptics.ts +38 -0
- package/src/services/mediaCompress.ts +7 -2
- package/src/services/resolveUrl.ts +2 -2
- package/src/services/workflowGate.ts +138 -0
- package/src/specs/MyazaEmrtd.nitro.ts +114 -0
- package/src/specs/MyazaFaceDetector.nitro.ts +10 -0
- package/src/specs/MyazaRectDetector.nitro.ts +50 -0
- package/src/specs/MyazaTextRecognizer.nitro.ts +48 -0
- package/src/store/derive.ts +225 -0
- package/src/store/kycStore.ts +250 -149
- package/src/store/state.ts +253 -0
- package/src/store/submit.ts +101 -0
- package/src/store/submitApplicant.ts +81 -0
- package/src/types/appearance.ts +113 -0
- package/src/types/business.ts +131 -0
- package/src/types/config.ts +150 -124
- package/src/types/id-types.ts +57 -0
- package/src/types/verification.ts +5 -0
- package/src/types/workflow.ts +141 -0
- package/src/utils/amountFormat.ts +73 -0
- package/src/utils/tokens.ts +4 -2
- package/src/__tests__/cardCrop.test.ts +0 -39
- package/src/__tests__/deviceMetadata.test.ts +0 -34
- package/src/__tests__/errors.test.ts +0 -34
- package/src/__tests__/flow.test.ts +0 -61
- package/src/__tests__/gestureDetector.test.ts +0 -37
- package/src/__tests__/liveness.test.ts +0 -112
- package/src/__tests__/resolveUrl.test.ts +0 -64
- package/src/__tests__/validators.test.ts +0 -38
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { 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 { MyazaInput } from '../components/MyazaInput';
|
|
9
|
+
import { MyazaSelect } from '../components/MyazaSelect';
|
|
10
|
+
import { CountryField } from '../components/CountryField';
|
|
11
|
+
import { CompanyInfoFields } from './CompanyInfoFields';
|
|
12
|
+
import {
|
|
13
|
+
businessCountriesFor,
|
|
14
|
+
businessCountryName,
|
|
15
|
+
businessProductsForCountry,
|
|
16
|
+
companyInfoFieldModes,
|
|
17
|
+
getBusinessProductDef,
|
|
18
|
+
keyPeopleNeedsContactEmail,
|
|
19
|
+
missingRequiredCompanyInfo,
|
|
20
|
+
} from '../config/business';
|
|
21
|
+
import { registrationNumberHint } from '../config/registrationHint';
|
|
22
|
+
import { isValidContactEmail } from '../config/contact';
|
|
23
|
+
import type { CompanyInfoField } from '../types/business';
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Business (KYB) registry details.
|
|
27
|
+
//
|
|
28
|
+
// The registry lookup verifies that a business EXISTS. Registration numbers are
|
|
29
|
+
// public, so it proves nothing about who is filling this in — that is what the
|
|
30
|
+
// rest of the KYB application (key people, documents, the applicant's own KYC)
|
|
31
|
+
// is for. This step collects the number, the registry country and product, and
|
|
32
|
+
// the company profile the workflow asks for.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
export const businessDetailsMeta = {
|
|
36
|
+
title: 'Business Details',
|
|
37
|
+
description:
|
|
38
|
+
'Provide your business registration details for verification against the official registry.',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function BusinessDetailsStep(): React.ReactElement {
|
|
42
|
+
const config = useKycConfig();
|
|
43
|
+
const store = useKycStore();
|
|
44
|
+
const { colors } = useTheme();
|
|
45
|
+
const business = useKyc((s) => s.business);
|
|
46
|
+
const [touched, setTouched] = useState(false);
|
|
47
|
+
|
|
48
|
+
const workflowBusiness = config.business;
|
|
49
|
+
const countries = useMemo(() => businessCountriesFor(workflowBusiness), [workflowBusiness]);
|
|
50
|
+
// Precedence mirrors the web SDK exactly: the visitor's pick, then the
|
|
51
|
+
// workflow's PRIMARY country, then the list head. The primary is always in
|
|
52
|
+
// the offered list but not necessarily FIRST — preferring `countries[0]`
|
|
53
|
+
// defaulted the picker to the wrong registry whenever the primary sat later.
|
|
54
|
+
const country = business.country ?? workflowBusiness?.country ?? countries[0] ?? '';
|
|
55
|
+
const products = useMemo(
|
|
56
|
+
() => businessProductsForCountry(workflowBusiness, country),
|
|
57
|
+
[workflowBusiness, country],
|
|
58
|
+
);
|
|
59
|
+
// Re-derived per country rather than remembered: a product the picked country
|
|
60
|
+
// does not offer would be rejected at submit.
|
|
61
|
+
const product = products.includes(business.product ?? '') ? business.product! : products[0]!;
|
|
62
|
+
const productDef = getBusinessProductDef(product);
|
|
63
|
+
|
|
64
|
+
const modes = companyInfoFieldModes(workflowBusiness);
|
|
65
|
+
const companyFields = (Object.keys(modes) as CompanyInfoField[]).filter(
|
|
66
|
+
(field) => modes[field] !== 'off',
|
|
67
|
+
);
|
|
68
|
+
const needsContactEmail = keyPeopleNeedsContactEmail(workflowBusiness);
|
|
69
|
+
|
|
70
|
+
const set = <K extends keyof typeof business>(key: K, value: (typeof business)[K]): void =>
|
|
71
|
+
store.getState().setBusinessField(key, value);
|
|
72
|
+
|
|
73
|
+
const missingCompanyInfo = missingRequiredCompanyInfo(workflowBusiness, {
|
|
74
|
+
address: business.address,
|
|
75
|
+
email: business.email,
|
|
76
|
+
phone: business.phone,
|
|
77
|
+
website: business.website,
|
|
78
|
+
});
|
|
79
|
+
const contactEmailInvalid =
|
|
80
|
+
needsContactEmail && business.contactEmail.trim() !== '' && !isValidContactEmail(business.contactEmail);
|
|
81
|
+
const nameMissing =
|
|
82
|
+
workflowBusiness?.requireRegistrationName === true && business.registrationName.trim() === '';
|
|
83
|
+
|
|
84
|
+
// Country-aware registration-number guidance (NG: CAC prefix rules + format
|
|
85
|
+
// validation; elsewhere: placeholder + registry tip). Mirrors the web SDK.
|
|
86
|
+
const regHint = registrationNumberHint(country, productDef);
|
|
87
|
+
const regNumber = business.registrationNumber;
|
|
88
|
+
const formatOk =
|
|
89
|
+
!regHint.isValidFormat || regNumber.trim() === '' || regHint.isValidFormat(regNumber);
|
|
90
|
+
const numberValid = regNumber.trim().length >= 2 && formatOk;
|
|
91
|
+
const canContinue =
|
|
92
|
+
numberValid && !nameMissing && missingCompanyInfo.length === 0 && !contactEmailInvalid;
|
|
93
|
+
|
|
94
|
+
const handleContinue = (): void => {
|
|
95
|
+
setTouched(true);
|
|
96
|
+
if (!canContinue) return;
|
|
97
|
+
// Persist the resolved country/product so the submission uses exactly what
|
|
98
|
+
// was on screen, not a re-derivation from a config that may have changed.
|
|
99
|
+
set('country', country);
|
|
100
|
+
set('product', product);
|
|
101
|
+
store.getState().nextStep();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<View>
|
|
106
|
+
{countries.length > 1 ? (
|
|
107
|
+
<>
|
|
108
|
+
<MyazaText variant="bodySmall" style={{ fontWeight: '600', marginBottom: spacing.xs }}>
|
|
109
|
+
Country of registration
|
|
110
|
+
</MyazaText>
|
|
111
|
+
{/* The SAME sheet as the phone field's dial-code picker — restricted
|
|
112
|
+
to the workflow's registry countries. */}
|
|
113
|
+
<CountryField
|
|
114
|
+
value={country}
|
|
115
|
+
options={countries.map((code) => ({ code, name: businessCountryName(code) }))}
|
|
116
|
+
onChange={(code) => {
|
|
117
|
+
set('country', code);
|
|
118
|
+
// The product list narrows per country, so a stale pick is
|
|
119
|
+
// cleared rather than carried into a refused submission.
|
|
120
|
+
set('product', null);
|
|
121
|
+
}}
|
|
122
|
+
/>
|
|
123
|
+
<View style={{ height: spacing.md }} />
|
|
124
|
+
</>
|
|
125
|
+
) : null}
|
|
126
|
+
|
|
127
|
+
{products.length > 1 ? (
|
|
128
|
+
<>
|
|
129
|
+
<MyazaText variant="bodySmall" style={{ fontWeight: '600', marginBottom: spacing.xs }}>
|
|
130
|
+
Verification type
|
|
131
|
+
</MyazaText>
|
|
132
|
+
<MyazaSelect
|
|
133
|
+
value={product}
|
|
134
|
+
sheetTitle="Verification type"
|
|
135
|
+
options={products.map((key) => ({
|
|
136
|
+
value: key,
|
|
137
|
+
label: getBusinessProductDef(key).label,
|
|
138
|
+
}))}
|
|
139
|
+
onChange={(key) => set('product', key)}
|
|
140
|
+
/>
|
|
141
|
+
<View style={{ height: spacing.md }} />
|
|
142
|
+
</>
|
|
143
|
+
) : null}
|
|
144
|
+
|
|
145
|
+
<MyazaInput
|
|
146
|
+
label={productDef.inputLabel}
|
|
147
|
+
value={business.registrationNumber}
|
|
148
|
+
onChangeText={(text) => set('registrationNumber', text)}
|
|
149
|
+
placeholder={regHint.placeholder}
|
|
150
|
+
autoCapitalize="characters"
|
|
151
|
+
autoFocus
|
|
152
|
+
// Live like the web SDK: a wrong CAC prefix is corrected on the spot,
|
|
153
|
+
// not discovered on Continue. The registry tip fills the same slot
|
|
154
|
+
// until there is an error to show.
|
|
155
|
+
error={
|
|
156
|
+
regNumber !== '' && !numberValid
|
|
157
|
+
? (!formatOk && regHint.formatError) ||
|
|
158
|
+
`Enter a valid ${productDef.inputLabel.toLowerCase()}.`
|
|
159
|
+
: null
|
|
160
|
+
}
|
|
161
|
+
helper={regHint.tip ?? undefined}
|
|
162
|
+
/>
|
|
163
|
+
|
|
164
|
+
<View style={{ height: spacing.sm }} />
|
|
165
|
+
<MyazaInput
|
|
166
|
+
label={`Registered business name${workflowBusiness?.requireRegistrationName ? '' : ' (optional)'}`}
|
|
167
|
+
value={business.registrationName}
|
|
168
|
+
onChangeText={(text) => set('registrationName', text)}
|
|
169
|
+
placeholder="Enter the registered business name"
|
|
170
|
+
error={touched && nameMissing ? 'Enter the registered business name.' : null}
|
|
171
|
+
/>
|
|
172
|
+
|
|
173
|
+
<CompanyInfoFields
|
|
174
|
+
fields={companyFields}
|
|
175
|
+
modes={modes}
|
|
176
|
+
values={{
|
|
177
|
+
address: business.address,
|
|
178
|
+
email: business.email,
|
|
179
|
+
phone: business.phone,
|
|
180
|
+
website: business.website,
|
|
181
|
+
}}
|
|
182
|
+
missing={missingCompanyInfo}
|
|
183
|
+
showErrors={touched}
|
|
184
|
+
onChange={(field, text) => set(field, text)}
|
|
185
|
+
/>
|
|
186
|
+
|
|
187
|
+
{needsContactEmail ? (
|
|
188
|
+
<>
|
|
189
|
+
<View style={{ height: spacing.sm }} />
|
|
190
|
+
<MyazaInput
|
|
191
|
+
label="Contact email for owner verification (optional)"
|
|
192
|
+
value={business.contactEmail}
|
|
193
|
+
onChangeText={(text) => set('contactEmail', text)}
|
|
194
|
+
placeholder="admin@company.com"
|
|
195
|
+
keyboardType="email-address"
|
|
196
|
+
autoCapitalize="none"
|
|
197
|
+
helper="We'll email this address a link for your directors and owners to verify their identity."
|
|
198
|
+
error={touched && contactEmailInvalid ? 'Enter a valid email address.' : null}
|
|
199
|
+
/>
|
|
200
|
+
</>
|
|
201
|
+
) : null}
|
|
202
|
+
|
|
203
|
+
<View style={{ height: spacing.md }} />
|
|
204
|
+
<MyazaButton label="Continue" onPress={handleContinue} disabled={touched && !canContinue} />
|
|
205
|
+
{touched && !canContinue ? (
|
|
206
|
+
<MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.xs, textAlign: 'center' }}>
|
|
207
|
+
Please complete the highlighted fields.
|
|
208
|
+
</MyazaText>
|
|
209
|
+
) : null}
|
|
210
|
+
</View>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ActivityIndicator, Image, 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
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// One business-document upload slot — mirrors the web SDK's
|
|
11
|
+
// BusinessDocumentSlot 1:1: empty, it is a dashed full-width tap target
|
|
12
|
+
// (upload glyph — a spinner while uploading — label with a red * when
|
|
13
|
+
// required, "Photo or PDF, up to 20MB"); filled, a solid card showing a 48px
|
|
14
|
+
// THUMBNAIL of what was actually picked (a muted file tile for PDFs, a check
|
|
15
|
+
// when the preview didn't survive a remount — web's exact fallback ladder),
|
|
16
|
+
// the file's name over the slot label, Replace, and remove. Showing the file
|
|
17
|
+
// back matters — it is the only way a user catches "wrong photo from the
|
|
18
|
+
// camera roll" before submitting.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/** Web parity: UploadedFileThumb is h-12 w-12 rounded-lg (48 / radius 8). */
|
|
22
|
+
const THUMB = 48;
|
|
23
|
+
|
|
24
|
+
function UploadedThumb({ previewUri, isPdf }: { previewUri: string | null; isPdf: boolean }): React.ReactElement {
|
|
25
|
+
const { colors } = useTheme();
|
|
26
|
+
if (previewUri && !isPdf) {
|
|
27
|
+
return (
|
|
28
|
+
<Image
|
|
29
|
+
source={{ uri: previewUri }}
|
|
30
|
+
resizeMode="cover"
|
|
31
|
+
style={{
|
|
32
|
+
width: THUMB,
|
|
33
|
+
height: THUMB,
|
|
34
|
+
borderRadius: radius.xs,
|
|
35
|
+
borderWidth: 1,
|
|
36
|
+
borderColor: colors.border,
|
|
37
|
+
}}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (isPdf) {
|
|
42
|
+
return (
|
|
43
|
+
<View
|
|
44
|
+
style={{
|
|
45
|
+
width: THUMB,
|
|
46
|
+
height: THUMB,
|
|
47
|
+
borderRadius: radius.xs,
|
|
48
|
+
borderWidth: 1,
|
|
49
|
+
borderColor: colors.border,
|
|
50
|
+
backgroundColor: colors.backgroundSecondary,
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
justifyContent: 'center',
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<Icon name="file-text" size={20} color={colors.textMuted} />
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
// The preview didn't survive a remount — the check still says "uploaded".
|
|
60
|
+
return <Icon name="check" size={20} color={colors.success} />;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function BusinessDocumentSlot({
|
|
64
|
+
label,
|
|
65
|
+
required,
|
|
66
|
+
fileName,
|
|
67
|
+
uploading,
|
|
68
|
+
previewUri = null,
|
|
69
|
+
isPdf = false,
|
|
70
|
+
onPick,
|
|
71
|
+
onRemove,
|
|
72
|
+
}: {
|
|
73
|
+
label: string;
|
|
74
|
+
required: boolean;
|
|
75
|
+
/** Uploaded file name, when this slot already has a mediaId. */
|
|
76
|
+
fileName: string | null;
|
|
77
|
+
uploading: boolean;
|
|
78
|
+
/** Local URI of the picked image, for the thumbnail (null once remounted). */
|
|
79
|
+
previewUri?: string | null;
|
|
80
|
+
isPdf?: boolean;
|
|
81
|
+
/** Open the photo/file source sheet (also serves Replace). */
|
|
82
|
+
onPick: () => void;
|
|
83
|
+
onRemove: () => void;
|
|
84
|
+
}): React.ReactElement {
|
|
85
|
+
const { colors } = useTheme();
|
|
86
|
+
|
|
87
|
+
if (fileName !== null && !uploading) {
|
|
88
|
+
return (
|
|
89
|
+
<View
|
|
90
|
+
style={{
|
|
91
|
+
flexDirection: 'row',
|
|
92
|
+
alignItems: 'center',
|
|
93
|
+
borderWidth: 1,
|
|
94
|
+
borderColor: colors.border,
|
|
95
|
+
borderRadius: radius.sm,
|
|
96
|
+
backgroundColor: colors.backgroundSecondary,
|
|
97
|
+
padding: spacing.md,
|
|
98
|
+
marginBottom: spacing.sm + 4,
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<UploadedThumb previewUri={previewUri} isPdf={isPdf} />
|
|
102
|
+
<View style={{ width: spacing.sm + 4 }} />
|
|
103
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
104
|
+
<MyazaText variant="label" style={{ fontWeight: '600' }} numberOfLines={1}>
|
|
105
|
+
{fileName}
|
|
106
|
+
</MyazaText>
|
|
107
|
+
<MyazaText variant="bodySmall" color={colors.textMuted}>
|
|
108
|
+
{label}
|
|
109
|
+
</MyazaText>
|
|
110
|
+
</View>
|
|
111
|
+
<Pressable onPress={onPick} hitSlop={8} accessibilityRole="button">
|
|
112
|
+
<MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>
|
|
113
|
+
Replace
|
|
114
|
+
</MyazaText>
|
|
115
|
+
</Pressable>
|
|
116
|
+
<View style={{ width: spacing.sm + 4 }} />
|
|
117
|
+
<Pressable
|
|
118
|
+
onPress={onRemove}
|
|
119
|
+
hitSlop={8}
|
|
120
|
+
accessibilityRole="button"
|
|
121
|
+
accessibilityLabel={`Remove ${label}`}
|
|
122
|
+
>
|
|
123
|
+
<Icon name="close" size={16} color={colors.textMuted} />
|
|
124
|
+
</Pressable>
|
|
125
|
+
</View>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<Pressable
|
|
131
|
+
onPress={uploading ? undefined : onPick}
|
|
132
|
+
accessibilityRole="button"
|
|
133
|
+
accessibilityState={{ disabled: uploading }}
|
|
134
|
+
style={{
|
|
135
|
+
flexDirection: 'row',
|
|
136
|
+
alignItems: 'center',
|
|
137
|
+
borderWidth: 2,
|
|
138
|
+
borderStyle: 'dashed',
|
|
139
|
+
borderColor: colors.border,
|
|
140
|
+
borderRadius: radius.sm,
|
|
141
|
+
padding: spacing.md,
|
|
142
|
+
marginBottom: spacing.sm + 4,
|
|
143
|
+
opacity: uploading ? 0.7 : 1,
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
{uploading ? (
|
|
147
|
+
<ActivityIndicator size="small" color={colors.primary} />
|
|
148
|
+
) : (
|
|
149
|
+
<Icon name="upload" size={20} color={colors.textMuted} />
|
|
150
|
+
)}
|
|
151
|
+
<View style={{ width: spacing.sm + 4 }} />
|
|
152
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
153
|
+
<MyazaText variant="label" style={{ fontWeight: '600' }} numberOfLines={1}>
|
|
154
|
+
{label}
|
|
155
|
+
{required ? (
|
|
156
|
+
<MyazaText variant="label" color={colors.error} style={{ fontWeight: '600' }}>
|
|
157
|
+
{' *'}
|
|
158
|
+
</MyazaText>
|
|
159
|
+
) : null}
|
|
160
|
+
</MyazaText>
|
|
161
|
+
<MyazaText variant="bodySmall" color={colors.textMuted}>
|
|
162
|
+
{uploading ? 'Uploading…' : 'Photo or PDF, up to 20MB'}
|
|
163
|
+
</MyazaText>
|
|
164
|
+
</View>
|
|
165
|
+
</Pressable>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import * as ImagePicker from 'expo-image-picker';
|
|
4
|
+
|
|
5
|
+
import { spacing } from '../config/theme';
|
|
6
|
+
import { useKyc, useKycConfig, useKycStore, useTheme } from '../components/runtime';
|
|
7
|
+
import { MyazaText } from '../components/Typography';
|
|
8
|
+
import { MyazaButton } from '../components/MyazaButton';
|
|
9
|
+
import { MediaSourceSheet } from '../components/MediaSourceSheet';
|
|
10
|
+
import { BusinessDocumentSlot } from './BusinessDocumentSlot';
|
|
11
|
+
import { withRetry } from '../services/retry';
|
|
12
|
+
import { compressDocumentImage } from '../services/mediaCompress';
|
|
13
|
+
import { resolveBusinessDocumentTypes, type ResolvedBusinessDocumentType } from '../config/businessSteps';
|
|
14
|
+
import { isAcceptedPoaMimeType, POA_ACCEPTED_MIME_TYPES } from '../config/proofOfAddress';
|
|
15
|
+
import { loadDocumentPicker } from '../services/documentPicker';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Supporting company documents (certificate of incorporation, MEMART, …).
|
|
19
|
+
//
|
|
20
|
+
// The registry says a business exists; these are what say the applicant holds
|
|
21
|
+
// its paperwork. The server OCRs each one and cross-checks it against the
|
|
22
|
+
// registry record — a soft result that never fails the verification, so the
|
|
23
|
+
// only thing blocking here is a REQUIRED slot left empty, which the server
|
|
24
|
+
// would 422 anyway.
|
|
25
|
+
//
|
|
26
|
+
// Layout mirrors the web SDK 1:1: the header carries the description, each
|
|
27
|
+
// document is a dashed upload slot (see BusinessDocumentSlot), and Continue is
|
|
28
|
+
// disabled until every required slot is filled. Tapping a slot opens the
|
|
29
|
+
// photo/file source sheet — the mobile stand-in for web's file input.
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export const businessDocumentsMeta = {
|
|
33
|
+
title: 'Business documents',
|
|
34
|
+
description:
|
|
35
|
+
'Upload the supporting documents for your business. Required documents are marked with *.',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function BusinessDocumentsStep(): React.ReactElement {
|
|
39
|
+
const config = useKycConfig();
|
|
40
|
+
const store = useKycStore();
|
|
41
|
+
const { colors } = useTheme();
|
|
42
|
+
const uploaded = useKyc((s) => s.businessApplication.documents);
|
|
43
|
+
const [busySlot, setBusySlot] = useState<string | null>(null);
|
|
44
|
+
const [pickingSlot, setPickingSlot] = useState<ResolvedBusinessDocumentType | null>(null);
|
|
45
|
+
const [error, setError] = useState<string | null>(null);
|
|
46
|
+
|
|
47
|
+
const slots = useMemo(
|
|
48
|
+
() => resolveBusinessDocumentTypes(config.business),
|
|
49
|
+
[config.business],
|
|
50
|
+
);
|
|
51
|
+
const missing = slots.filter((s) => s.required && !uploaded.some((d) => d.type === s.key));
|
|
52
|
+
|
|
53
|
+
const attach = useCallback(
|
|
54
|
+
async (slot: ResolvedBusinessDocumentType, uri: string, mimeType: string | undefined, name: string) => {
|
|
55
|
+
setBusySlot(slot.key);
|
|
56
|
+
setError(null);
|
|
57
|
+
try {
|
|
58
|
+
// A PDF is passed through untouched — re-encoding would destroy the
|
|
59
|
+
// text the server reads off it.
|
|
60
|
+
const isPdf = (mimeType ?? '').toLowerCase().startsWith('application/pdf');
|
|
61
|
+
const finalUri = isPdf ? uri : await compressDocumentImage(uri).catch(() => uri);
|
|
62
|
+
const mediaId = await withRetry(() =>
|
|
63
|
+
store.getState().api.upload({ uri: finalUri, type: mimeType, name }, 'business_document'),
|
|
64
|
+
);
|
|
65
|
+
// Preview details ride the RECORD so the thumbnail survives leaving
|
|
66
|
+
// the step — screen-local state dies with the component.
|
|
67
|
+
store.getState().setBusinessDocument({
|
|
68
|
+
type: slot.key,
|
|
69
|
+
mediaId,
|
|
70
|
+
fileName: name,
|
|
71
|
+
...(isPdf ? { isPdf: true } : { previewUri: finalUri }),
|
|
72
|
+
});
|
|
73
|
+
} catch {
|
|
74
|
+
setError(`We could not upload ${slot.label.toLowerCase()}. Please try again.`);
|
|
75
|
+
} finally {
|
|
76
|
+
setBusySlot(null);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
[store],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const takePhoto = useCallback(
|
|
83
|
+
async (slot: ResolvedBusinessDocumentType) => {
|
|
84
|
+
const permission = await ImagePicker.requestCameraPermissionsAsync();
|
|
85
|
+
if (!permission.granted) {
|
|
86
|
+
setError('Camera access is needed to photograph the document.');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const result = await ImagePicker.launchCameraAsync({ mediaTypes: ['images'], quality: 1 });
|
|
90
|
+
const asset = result.canceled ? undefined : result.assets[0];
|
|
91
|
+
if (asset) {
|
|
92
|
+
// A FRIENDLY name, not the picker's temp junk — camera/library assets
|
|
93
|
+
// carry generated names; the slot key says what the file IS.
|
|
94
|
+
await attach(slot, asset.uri, asset.mimeType ?? 'image/jpeg', `${slot.key}.jpg`);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
[attach],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const choosePhoto = useCallback(
|
|
101
|
+
async (slot: ResolvedBusinessDocumentType) => {
|
|
102
|
+
const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], quality: 1 });
|
|
103
|
+
const asset = result.canceled ? undefined : result.assets[0];
|
|
104
|
+
if (asset) {
|
|
105
|
+
await attach(slot, asset.uri, asset.mimeType ?? 'image/jpeg', `${slot.key}.jpg`);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
[attach],
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const chooseFile = useCallback(
|
|
112
|
+
async (slot: ResolvedBusinessDocumentType) => {
|
|
113
|
+
const picker = loadDocumentPicker();
|
|
114
|
+
if (!picker) {
|
|
115
|
+
setError('Choosing a file is not available in this app. Please photograph the document.');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const result = await picker.getDocumentAsync({
|
|
119
|
+
type: [...POA_ACCEPTED_MIME_TYPES],
|
|
120
|
+
copyToCacheDirectory: true,
|
|
121
|
+
multiple: false,
|
|
122
|
+
});
|
|
123
|
+
const asset = result.canceled ? undefined : result.assets?.[0];
|
|
124
|
+
if (!asset) return;
|
|
125
|
+
if (!isAcceptedPoaMimeType(asset.mimeType)) {
|
|
126
|
+
setError('Please choose a photo (JPEG, PNG or WebP) or a PDF.');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
await attach(slot, asset.uri, asset.mimeType, asset.name ?? slot.key);
|
|
130
|
+
},
|
|
131
|
+
[attach],
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const handleContinue = (): void => {
|
|
135
|
+
if (missing.length > 0 || busySlot !== null) return;
|
|
136
|
+
store.getState().nextStep();
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<View>
|
|
141
|
+
{slots.map((slot) => {
|
|
142
|
+
const doc = uploaded.find((d) => d.type === slot.key);
|
|
143
|
+
return (
|
|
144
|
+
<BusinessDocumentSlot
|
|
145
|
+
key={slot.key}
|
|
146
|
+
label={slot.label}
|
|
147
|
+
required={slot.required}
|
|
148
|
+
fileName={doc?.fileName ?? null}
|
|
149
|
+
uploading={busySlot === slot.key}
|
|
150
|
+
previewUri={doc?.previewUri ?? null}
|
|
151
|
+
isPdf={doc?.isPdf === true}
|
|
152
|
+
onPick={() => setPickingSlot(slot)}
|
|
153
|
+
onRemove={() => store.getState().removeBusinessDocument(slot.key)}
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
})}
|
|
157
|
+
|
|
158
|
+
{error ? (
|
|
159
|
+
<MyazaText variant="bodySmall" color={colors.error} style={{ marginBottom: spacing.sm }}>
|
|
160
|
+
{error}
|
|
161
|
+
</MyazaText>
|
|
162
|
+
) : null}
|
|
163
|
+
|
|
164
|
+
<View style={{ height: spacing.xs }} />
|
|
165
|
+
<MyazaButton
|
|
166
|
+
label="Continue"
|
|
167
|
+
onPress={handleContinue}
|
|
168
|
+
disabled={missing.length > 0 || busySlot !== null}
|
|
169
|
+
/>
|
|
170
|
+
|
|
171
|
+
{/* THE SAME sheet as the Proof of Address step — identical title,
|
|
172
|
+
options, icons and captions, so uploading a document feels the same
|
|
173
|
+
everywhere in the flow. */}
|
|
174
|
+
<MediaSourceSheet
|
|
175
|
+
open={pickingSlot !== null}
|
|
176
|
+
title="Upload your document"
|
|
177
|
+
onClose={() => setPickingSlot(null)}
|
|
178
|
+
options={[
|
|
179
|
+
{
|
|
180
|
+
icon: 'image',
|
|
181
|
+
label: 'Photo library',
|
|
182
|
+
caption: 'Pick a photo you already have',
|
|
183
|
+
onPress: () => {
|
|
184
|
+
const slot = pickingSlot;
|
|
185
|
+
setPickingSlot(null);
|
|
186
|
+
if (slot) void choosePhoto(slot);
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
icon: 'camera',
|
|
191
|
+
label: 'Take a photo',
|
|
192
|
+
caption: 'Photograph the document now',
|
|
193
|
+
onPress: () => {
|
|
194
|
+
const slot = pickingSlot;
|
|
195
|
+
setPickingSlot(null);
|
|
196
|
+
if (slot) void takePhoto(slot);
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
icon: 'file-text',
|
|
201
|
+
label: 'Choose a file',
|
|
202
|
+
caption: 'A PDF or image from your files',
|
|
203
|
+
onPress: () => {
|
|
204
|
+
const slot = pickingSlot;
|
|
205
|
+
setPickingSlot(null);
|
|
206
|
+
if (slot) void chooseFile(slot);
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
]}
|
|
210
|
+
/>
|
|
211
|
+
</View>
|
|
212
|
+
);
|
|
213
|
+
}
|