@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,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
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { 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 { ContactCodeEntry } from '../components/ContactCodeEntry';
|
|
8
|
+
import { ExpiryCountdown } from '../components/ExpiryCountdown';
|
|
9
|
+
import { CHANNEL_LABELS, type PhoneOtpChannel } from '../config/contact';
|
|
10
|
+
import type { OtpInputStyle } from '../types/workflow';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// The second half of a contact check: the code has been sent, now enter it.
|
|
14
|
+
//
|
|
15
|
+
// Split from ContactVerificationStep (200-line rule). Auto-submits on the last
|
|
16
|
+
// digit — the user has already typed the whole code by then, and making them
|
|
17
|
+
// press a button as well is a step for nothing.
|
|
18
|
+
//
|
|
19
|
+
// Layout mirrors the web SDK: the code field, then a single row carrying the
|
|
20
|
+
// live expiry on the left and Resend on the right, then the switch-channel
|
|
21
|
+
// escape hatch. A code that never arrives is usually a channel problem rather
|
|
22
|
+
// than a typo, so resending down the same channel is rarely the fix.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export function ContactCodeStep({
|
|
26
|
+
codeLength,
|
|
27
|
+
inputStyle,
|
|
28
|
+
code,
|
|
29
|
+
busy,
|
|
30
|
+
error,
|
|
31
|
+
attemptsLeft,
|
|
32
|
+
expiresAt,
|
|
33
|
+
otherChannel,
|
|
34
|
+
onChange,
|
|
35
|
+
onCheck,
|
|
36
|
+
onResend,
|
|
37
|
+
}: {
|
|
38
|
+
codeLength: number;
|
|
39
|
+
inputStyle: OtpInputStyle;
|
|
40
|
+
code: string;
|
|
41
|
+
busy: boolean;
|
|
42
|
+
error: string | null;
|
|
43
|
+
attemptsLeft?: number;
|
|
44
|
+
/** ISO expiry from the send response; null falls back to static copy. */
|
|
45
|
+
expiresAt: string | null;
|
|
46
|
+
/** The channel NOT in use, when the workflow offers a second one. */
|
|
47
|
+
otherChannel: PhoneOtpChannel | null;
|
|
48
|
+
onChange: (code: string) => void;
|
|
49
|
+
onCheck: (code: string) => void;
|
|
50
|
+
/** Resends; with a channel, switches to it first. */
|
|
51
|
+
onResend: (switchTo?: PhoneOtpChannel) => void;
|
|
52
|
+
}): React.ReactElement {
|
|
53
|
+
const { colors } = useTheme();
|
|
54
|
+
// No "enter the code we sent to …" line here: the sheet header carries it
|
|
55
|
+
// (see contactMeta), matching the web SDK. Two copies drifted apart.
|
|
56
|
+
return (
|
|
57
|
+
<View>
|
|
58
|
+
<ContactCodeEntry
|
|
59
|
+
code={code}
|
|
60
|
+
onChange={onChange}
|
|
61
|
+
codeLength={codeLength}
|
|
62
|
+
style={inputStyle}
|
|
63
|
+
disabled={busy}
|
|
64
|
+
onComplete={onCheck}
|
|
65
|
+
/>
|
|
66
|
+
|
|
67
|
+
<View style={{ height: spacing.sm }} />
|
|
68
|
+
<View
|
|
69
|
+
style={{
|
|
70
|
+
flexDirection: 'row',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
justifyContent: 'space-between',
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
{expiresAt ? (
|
|
76
|
+
<ExpiryCountdown expiresAt={expiresAt} />
|
|
77
|
+
) : (
|
|
78
|
+
<MyazaText variant="bodySmall" color={colors.textMuted}>
|
|
79
|
+
The code expires in 5 minutes.
|
|
80
|
+
</MyazaText>
|
|
81
|
+
)}
|
|
82
|
+
<Pressable
|
|
83
|
+
onPress={() => onResend()}
|
|
84
|
+
disabled={busy}
|
|
85
|
+
accessibilityRole="button"
|
|
86
|
+
hitSlop={8}
|
|
87
|
+
>
|
|
88
|
+
<MyazaText
|
|
89
|
+
variant="bodySmall"
|
|
90
|
+
color={busy ? colors.textMuted : colors.primary}
|
|
91
|
+
style={{ fontWeight: '600' }}
|
|
92
|
+
>
|
|
93
|
+
Resend code
|
|
94
|
+
</MyazaText>
|
|
95
|
+
</Pressable>
|
|
96
|
+
</View>
|
|
97
|
+
|
|
98
|
+
{otherChannel ? (
|
|
99
|
+
<>
|
|
100
|
+
<View style={{ height: spacing.sm }} />
|
|
101
|
+
<Pressable
|
|
102
|
+
onPress={() => onResend(otherChannel)}
|
|
103
|
+
disabled={busy}
|
|
104
|
+
accessibilityRole="button"
|
|
105
|
+
hitSlop={8}
|
|
106
|
+
>
|
|
107
|
+
<MyazaText
|
|
108
|
+
variant="bodySmall"
|
|
109
|
+
color={colors.textMuted}
|
|
110
|
+
style={{ textAlign: 'center' }}
|
|
111
|
+
>
|
|
112
|
+
{`Didn't get it? Send by ${CHANNEL_LABELS[otherChannel]} instead`}
|
|
113
|
+
</MyazaText>
|
|
114
|
+
</Pressable>
|
|
115
|
+
</>
|
|
116
|
+
) : null}
|
|
117
|
+
|
|
118
|
+
{error ? (
|
|
119
|
+
<MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.sm }}>
|
|
120
|
+
{/* The remaining count is what stops someone burning the whole
|
|
121
|
+
budget guessing — without it there is no signal the code is
|
|
122
|
+
about to die. */}
|
|
123
|
+
{attemptsLeft !== undefined
|
|
124
|
+
? `${error} ${attemptsLeft} ${attemptsLeft === 1 ? 'attempt' : 'attempts'} left.`
|
|
125
|
+
: error}
|
|
126
|
+
</MyazaText>
|
|
127
|
+
) : null}
|
|
128
|
+
</View>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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 { PhoneNumberInput } from '../components/PhoneNumberInput';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* "Where should we send it": an email field, or the dial-code phone input that
|
|
12
|
+
* formats nationally and emits E.164. Mirrors the web SDK's component of the
|
|
13
|
+
* same name and Flutter's ContactDestinationField.
|
|
14
|
+
*/
|
|
15
|
+
export function ContactDestinationField({
|
|
16
|
+
isEmail,
|
|
17
|
+
email,
|
|
18
|
+
onEmailChange,
|
|
19
|
+
onPhoneChange,
|
|
20
|
+
defaultCountry,
|
|
21
|
+
error,
|
|
22
|
+
disabled,
|
|
23
|
+
}: {
|
|
24
|
+
isEmail: boolean;
|
|
25
|
+
email: string;
|
|
26
|
+
onEmailChange: (value: string) => void;
|
|
27
|
+
onPhoneChange: (value: { e164: string; isValid: boolean }) => void;
|
|
28
|
+
defaultCountry?: string;
|
|
29
|
+
error: string | null;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
}): React.ReactElement {
|
|
32
|
+
if (isEmail) {
|
|
33
|
+
return (
|
|
34
|
+
<MyazaInput
|
|
35
|
+
label="Email address"
|
|
36
|
+
value={email}
|
|
37
|
+
onChangeText={onEmailChange}
|
|
38
|
+
placeholder="you@example.com"
|
|
39
|
+
keyboardType="email-address"
|
|
40
|
+
autoCapitalize="none"
|
|
41
|
+
error={error}
|
|
42
|
+
editable={!disabled}
|
|
43
|
+
autoFocus
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<View>
|
|
50
|
+
<MyazaText variant="label" style={{ marginBottom: spacing.xs }}>
|
|
51
|
+
Phone number
|
|
52
|
+
</MyazaText>
|
|
53
|
+
<PhoneNumberInput
|
|
54
|
+
defaultCountry={defaultCountry}
|
|
55
|
+
disabled={disabled}
|
|
56
|
+
onChange={onPhoneChange}
|
|
57
|
+
/>
|
|
58
|
+
{error ? <ErrorLine text={error} /> : null}
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ErrorLine({ text }: { text: string }): React.ReactElement {
|
|
64
|
+
const { colors } = useTheme();
|
|
65
|
+
return (
|
|
66
|
+
<MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.xs }}>
|
|
67
|
+
{text}
|
|
68
|
+
</MyazaText>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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 { Icon } from '../components/Icon';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The reassurance line under the contact step's actions, matching the web SDK.
|
|
11
|
+
*
|
|
12
|
+
* Small, but it answers the question the step provokes: an email step is asked
|
|
13
|
+
* "what will you do with my address", a phone step "will this cost me money".
|
|
14
|
+
*/
|
|
15
|
+
export function ContactFooterNote({ isEmail }: { isEmail: boolean }): React.ReactElement {
|
|
16
|
+
const { colors } = useTheme();
|
|
17
|
+
return (
|
|
18
|
+
<View
|
|
19
|
+
style={{
|
|
20
|
+
flexDirection: 'row',
|
|
21
|
+
alignItems: 'center',
|
|
22
|
+
justifyContent: 'center',
|
|
23
|
+
gap: spacing.xs,
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
<Icon name={isEmail ? 'mail' : 'smartphone'} size={12} color={colors.textMuted} />
|
|
27
|
+
<MyazaText variant="bodySmall" color={colors.textMuted}>
|
|
28
|
+
{isEmail
|
|
29
|
+
? 'We only use this to verify your identity.'
|
|
30
|
+
: 'Standard message rates may apply.'}
|
|
31
|
+
</MyazaText>
|
|
32
|
+
</View>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../config/theme';
|
|
5
|
+
import { useEffectiveCountry, useKyc, useKycConfig, useKycStore } from '../components/runtime';
|
|
6
|
+
import { MyazaButton } from '../components/MyazaButton';
|
|
7
|
+
import { ContactDestinationField } from './ContactDestinationField';
|
|
8
|
+
import { ContactVerifiedPanel } from './ContactVerifiedPanel';
|
|
9
|
+
import { ContactCodeStep } from './ContactCodeStep';
|
|
10
|
+
import { ContactChannelChoice } from './ContactChannelChoice';
|
|
11
|
+
import { ContactFooterNote } from './ContactFooterNote';
|
|
12
|
+
import {
|
|
13
|
+
contactCodeLength,
|
|
14
|
+
contactIsRequired,
|
|
15
|
+
isValidContactEmail,
|
|
16
|
+
offeredPhoneChannels,
|
|
17
|
+
type PhoneOtpChannel,
|
|
18
|
+
} from '../config/contact';
|
|
19
|
+
import { attemptsRemaining, describeCheckError, describeSendError } from '../services/contactErrors';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Contact-verification OTP step — email or phone, one component, two mounts.
|
|
23
|
+
//
|
|
24
|
+
// enter destination → send → code entry → verified → continue
|
|
25
|
+
//
|
|
26
|
+
// The proof token is single-use and rides the /verify submission. The server
|
|
27
|
+
// enforces whether it was required; the `required: false` skip here is UX, and
|
|
28
|
+
// a skipped optional check simply submits without the proof.
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
export function ContactVerificationStep({
|
|
32
|
+
channel,
|
|
33
|
+
}: {
|
|
34
|
+
channel: 'email' | 'phone';
|
|
35
|
+
}): React.ReactElement {
|
|
36
|
+
const config = useKycConfig();
|
|
37
|
+
const country = useEffectiveCountry();
|
|
38
|
+
const store = useKycStore();
|
|
39
|
+
const contact = useKyc((s) => s.contact);
|
|
40
|
+
|
|
41
|
+
const isEmail = channel === 'email';
|
|
42
|
+
const stepConfig = isEmail ? config.emailVerification : config.phoneVerification;
|
|
43
|
+
const required = contactIsRequired(stepConfig);
|
|
44
|
+
const codeLength = contactCodeLength(stepConfig);
|
|
45
|
+
const inputStyle = stepConfig?.inputStyle ?? 'segmented';
|
|
46
|
+
// The org decides which channels are on offer; the user picks between them.
|
|
47
|
+
const offeredChannels: PhoneOtpChannel[] = isEmail
|
|
48
|
+
? []
|
|
49
|
+
: offeredPhoneChannels(config.phoneVerification?.channels);
|
|
50
|
+
const [via, setVia] = useState<PhoneOtpChannel>(offeredChannels[0] ?? 'sms');
|
|
51
|
+
|
|
52
|
+
const alreadyVerified = isEmail ? contact.emailToken != null : contact.phoneToken != null;
|
|
53
|
+
|
|
54
|
+
const [destination, setDestination] = useState(contact.emailAddress ?? '');
|
|
55
|
+
// The phone field emits E.164 + validity from libphonenumber, so the step no
|
|
56
|
+
// longer guesses either from raw text.
|
|
57
|
+
const [phone, setPhone] = useState<{ e164: string; isValid: boolean }>({
|
|
58
|
+
e164: contact.phoneNumber ?? '',
|
|
59
|
+
isValid: false,
|
|
60
|
+
});
|
|
61
|
+
const [challengeId, setChallengeId] = useState<string | null>(null);
|
|
62
|
+
const [expiresAt, setExpiresAt] = useState<string | null>(null);
|
|
63
|
+
const [code, setCode] = useState('');
|
|
64
|
+
const [busy, setBusy] = useState(false);
|
|
65
|
+
const [error, setError] = useState<string | null>(null);
|
|
66
|
+
const [attemptsLeft, setAttemptsLeft] = useState<number | undefined>(undefined);
|
|
67
|
+
const otherChannel = offeredChannels.find((c) => c !== via) ?? null;
|
|
68
|
+
|
|
69
|
+
const value = isEmail ? destination.trim() : phone.e164;
|
|
70
|
+
const canSend = isEmail ? isValidContactEmail(value) : phone.isValid;
|
|
71
|
+
|
|
72
|
+
const advance = useCallback(() => store.getState().nextStep(), [store]);
|
|
73
|
+
|
|
74
|
+
// The sheet header is rendered by the shell and cannot see this step's state,
|
|
75
|
+
// so publish what it needs to caption: the channel in play now, and the
|
|
76
|
+
// destination once a code is actually out. Re-published when the user picks a
|
|
77
|
+
// different delivery channel so the header's "by WhatsApp" follows the choice.
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
store.getState().setContactChallenge({
|
|
80
|
+
channel,
|
|
81
|
+
...(isEmail ? {} : { via }),
|
|
82
|
+
...(challengeId ? { destination: value } : {}),
|
|
83
|
+
});
|
|
84
|
+
}, [store, channel, isEmail, via, challengeId, value]);
|
|
85
|
+
|
|
86
|
+
/** `switchTo` re-sends over the other channel when the first one didn't land. */
|
|
87
|
+
const send = async (switchTo?: PhoneOtpChannel): Promise<void> => {
|
|
88
|
+
const sendVia = switchTo ?? via;
|
|
89
|
+
if (switchTo) setVia(switchTo);
|
|
90
|
+
setBusy(true);
|
|
91
|
+
setError(null);
|
|
92
|
+
setAttemptsLeft(undefined);
|
|
93
|
+
try {
|
|
94
|
+
const res = await store.getState().api.contactSend({
|
|
95
|
+
channel,
|
|
96
|
+
destination: value,
|
|
97
|
+
codeLength,
|
|
98
|
+
...(stepConfig?.maxAttempts != null ? { maxAttempts: stepConfig.maxAttempts } : {}),
|
|
99
|
+
...(!isEmail && country ? { country } : {}),
|
|
100
|
+
...(isEmail ? {} : { via: sendVia }),
|
|
101
|
+
});
|
|
102
|
+
// Remember what was typed BEFORE the code lands: a user who backs out
|
|
103
|
+
// here and returns should not have to retype it.
|
|
104
|
+
store.getState().setContactDestination(channel, value);
|
|
105
|
+
setChallengeId(res.challengeId);
|
|
106
|
+
setExpiresAt(res.expiresAt ?? null);
|
|
107
|
+
setCode('');
|
|
108
|
+
} catch (err) {
|
|
109
|
+
setError(describeSendError(err));
|
|
110
|
+
} finally {
|
|
111
|
+
setBusy(false);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const check = async (submitted: string): Promise<void> => {
|
|
116
|
+
if (!challengeId || submitted.length !== codeLength) return;
|
|
117
|
+
setBusy(true);
|
|
118
|
+
setError(null);
|
|
119
|
+
try {
|
|
120
|
+
const res = await store.getState().api.contactCheck({ challengeId, code: submitted });
|
|
121
|
+
store.getState().setContactVerified(channel, value, res.token);
|
|
122
|
+
advance();
|
|
123
|
+
} catch (err) {
|
|
124
|
+
setError(describeCheckError(err));
|
|
125
|
+
setAttemptsLeft(attemptsRemaining(err));
|
|
126
|
+
setCode('');
|
|
127
|
+
} finally {
|
|
128
|
+
setBusy(false);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Returning to a step already passed — nothing left to do here.
|
|
133
|
+
if (alreadyVerified) {
|
|
134
|
+
return (
|
|
135
|
+
<ContactVerifiedPanel
|
|
136
|
+
isEmail={isEmail}
|
|
137
|
+
destination={isEmail ? contact.emailAddress : contact.phoneNumber}
|
|
138
|
+
onContinue={advance}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// One frame for both halves, like web: the panel swaps, the actions stay put.
|
|
144
|
+
return (
|
|
145
|
+
<View>
|
|
146
|
+
{challengeId ? (
|
|
147
|
+
<ContactCodeStep
|
|
148
|
+
codeLength={codeLength}
|
|
149
|
+
inputStyle={inputStyle}
|
|
150
|
+
code={code}
|
|
151
|
+
busy={busy}
|
|
152
|
+
error={error}
|
|
153
|
+
attemptsLeft={attemptsLeft}
|
|
154
|
+
expiresAt={expiresAt}
|
|
155
|
+
otherChannel={otherChannel}
|
|
156
|
+
onChange={setCode}
|
|
157
|
+
onCheck={(full) => void check(full)}
|
|
158
|
+
onResend={(switchTo) => void send(switchTo)}
|
|
159
|
+
/>
|
|
160
|
+
) : (
|
|
161
|
+
<>
|
|
162
|
+
<ContactDestinationField
|
|
163
|
+
isEmail={isEmail}
|
|
164
|
+
email={destination}
|
|
165
|
+
onEmailChange={setDestination}
|
|
166
|
+
onPhoneChange={setPhone}
|
|
167
|
+
defaultCountry={config.phoneVerification?.defaultCountry ?? country ?? undefined}
|
|
168
|
+
error={error}
|
|
169
|
+
disabled={busy}
|
|
170
|
+
/>
|
|
171
|
+
<ContactChannelChoice
|
|
172
|
+
offered={offeredChannels}
|
|
173
|
+
picked={via}
|
|
174
|
+
disabled={busy}
|
|
175
|
+
onPick={setVia}
|
|
176
|
+
/>
|
|
177
|
+
</>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
<View style={{ height: spacing.md }} />
|
|
181
|
+
<MyazaButton
|
|
182
|
+
label={challengeId ? 'Verify code' : 'Send code'}
|
|
183
|
+
loading={busy}
|
|
184
|
+
disabled={challengeId ? code.length !== codeLength : !canSend}
|
|
185
|
+
onPress={() => (challengeId ? void check(code) : void send())}
|
|
186
|
+
/>
|
|
187
|
+
|
|
188
|
+
{!required ? (
|
|
189
|
+
<>
|
|
190
|
+
<View style={{ height: spacing.sm }} />
|
|
191
|
+
<MyazaButton label="Skip for now" variant="ghost" disabled={busy} onPress={advance} />
|
|
192
|
+
</>
|
|
193
|
+
) : null}
|
|
194
|
+
|
|
195
|
+
<View style={{ height: spacing.md }} />
|
|
196
|
+
<ContactFooterNote isEmail={isEmail} />
|
|
197
|
+
</View>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../config/theme';
|
|
5
|
+
import { MyazaButton } from '../components/MyazaButton';
|
|
6
|
+
import { VerifiedNotice } from '../components/VerifiedNotice';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shown when the user returns to a contact step they already passed: the
|
|
10
|
+
* confirmation card plus Continue. Mirrors the web SDK's verified branch and
|
|
11
|
+
* Flutter's ContactVerifiedView.
|
|
12
|
+
*/
|
|
13
|
+
export function ContactVerifiedPanel({
|
|
14
|
+
isEmail,
|
|
15
|
+
destination,
|
|
16
|
+
onContinue,
|
|
17
|
+
}: {
|
|
18
|
+
isEmail: boolean;
|
|
19
|
+
/** The verified address/number, when known — shown verbatim, like web. */
|
|
20
|
+
destination?: string;
|
|
21
|
+
onContinue: () => void;
|
|
22
|
+
}): React.ReactElement {
|
|
23
|
+
return (
|
|
24
|
+
<View>
|
|
25
|
+
<VerifiedNotice
|
|
26
|
+
label={
|
|
27
|
+
destination
|
|
28
|
+
? `${destination} is verified.`
|
|
29
|
+
: `Your ${isEmail ? 'email' : 'phone number'} is verified.`
|
|
30
|
+
}
|
|
31
|
+
/>
|
|
32
|
+
<View style={{ height: spacing.md }} />
|
|
33
|
+
<MyazaButton label="Continue" onPress={onContinue} />
|
|
34
|
+
</View>
|
|
35
|
+
);
|
|
36
|
+
}
|