@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,149 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getCountries,
|
|
6
|
+
getCountryCallingCode,
|
|
7
|
+
parsePhoneNumberFromString,
|
|
8
|
+
type CountryCode,
|
|
9
|
+
} from 'libphonenumber-js/min';
|
|
10
|
+
|
|
11
|
+
import { radius, sizing, spacing } from '../config/theme';
|
|
12
|
+
import { useTheme } from './runtime';
|
|
13
|
+
import { MyazaText } from './Typography';
|
|
14
|
+
import { MyazaInput } from './MyazaInput';
|
|
15
|
+
import { Icon } from './Icon';
|
|
16
|
+
import { CountryFlag } from './CountryFlag';
|
|
17
|
+
import { DialCodePicker, type DialCodeOption } from './DialCodePicker';
|
|
18
|
+
import { formatNationalNumber } from '../config/phone';
|
|
19
|
+
// Names come from the generated table, NOT Intl.DisplayNames: Hermes may ship
|
|
20
|
+
// without region data, and every row then reads as a bare ISO code.
|
|
21
|
+
import { COUNTRY_NAMES } from '../config/countryNames.g';
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Phone field: a dial-code country picker beside a nationally-formatted number.
|
|
25
|
+
//
|
|
26
|
+
// The RN mirror of the web SDK's PhoneNumberInput and Flutter's
|
|
27
|
+
// phone_number_input.dart. Before this the step was a bare text field asking
|
|
28
|
+
// for "+2348012345678" — the user had to know their own country code and type
|
|
29
|
+
// it, and validity was a length guess.
|
|
30
|
+
//
|
|
31
|
+
// Formatting and validity both come from libphonenumber: "8031234567" shows as
|
|
32
|
+
// "803 123 4567" for NG, and a wrong-length number is rejected by the country's
|
|
33
|
+
// actual numbering plan rather than a 6–15 digit heuristic.
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export function PhoneNumberInput({
|
|
37
|
+
defaultCountry,
|
|
38
|
+
disabled,
|
|
39
|
+
onChange,
|
|
40
|
+
}: {
|
|
41
|
+
/** Seeds the picker (ISO-2). Falls back to NG. */
|
|
42
|
+
defaultCountry?: string;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
/** Fires on every edit with the E.164 value ('' until parseable) + validity. */
|
|
45
|
+
onChange: (value: { e164: string; isValid: boolean; country: string }) => void;
|
|
46
|
+
}): React.ReactElement {
|
|
47
|
+
const { colors } = useTheme();
|
|
48
|
+
|
|
49
|
+
const options = useMemo<DialCodeOption[]>(
|
|
50
|
+
() =>
|
|
51
|
+
getCountries()
|
|
52
|
+
// Only countries the shared table can name. libphonenumber lists five
|
|
53
|
+
// that it cannot (AC, CC, CX, IO, TA — dependencies with populations in
|
|
54
|
+
// the hundreds, reachable on their parent country's code anyway), and a
|
|
55
|
+
// row reading "AC" is worse than no row. Dropping them also makes this
|
|
56
|
+
// list exactly Flutter's, which is generated from the same table.
|
|
57
|
+
.filter((code) => COUNTRY_NAMES[code])
|
|
58
|
+
.map((code) => ({
|
|
59
|
+
code,
|
|
60
|
+
name: COUNTRY_NAMES[code] as string,
|
|
61
|
+
dialCode: `+${getCountryCallingCode(code)}`,
|
|
62
|
+
}))
|
|
63
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
64
|
+
[],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const seed = (defaultCountry?.toUpperCase() ?? 'NG') as CountryCode;
|
|
68
|
+
const [country, setCountry] = useState<CountryCode>(
|
|
69
|
+
options.some((o) => o.code === seed) ? seed : ('NG' as CountryCode),
|
|
70
|
+
);
|
|
71
|
+
const [national, setNational] = useState('');
|
|
72
|
+
const [open, setOpen] = useState(false);
|
|
73
|
+
|
|
74
|
+
const selected = options.find((o) => o.code === country);
|
|
75
|
+
|
|
76
|
+
const emit = (nextCountry: CountryCode, nextNational: string): void => {
|
|
77
|
+
const parsed = parsePhoneNumberFromString(nextNational, nextCountry);
|
|
78
|
+
onChange({
|
|
79
|
+
e164: parsed?.number ?? '',
|
|
80
|
+
isValid: parsed?.isValid() ?? false,
|
|
81
|
+
country: nextCountry,
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// The caret sits at the end: separators shift on nearly every keystroke, so
|
|
86
|
+
// tracking an interior caret fights the formatter more than it helps.
|
|
87
|
+
const onTyped = (raw: string): void => {
|
|
88
|
+
const formatted = formatNationalNumber(raw, country);
|
|
89
|
+
setNational(formatted);
|
|
90
|
+
emit(country, formatted);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const pickCountry = (code: string): void => {
|
|
94
|
+
const next = code as CountryCode;
|
|
95
|
+
setCountry(next);
|
|
96
|
+
setOpen(false);
|
|
97
|
+
// Re-group the digits already typed under the new country's plan.
|
|
98
|
+
const regrouped = formatNationalNumber(national, next);
|
|
99
|
+
setNational(regrouped);
|
|
100
|
+
emit(next, regrouped);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<View>
|
|
105
|
+
<View style={{ flexDirection: 'row', gap: spacing.sm }}>
|
|
106
|
+
<Pressable
|
|
107
|
+
onPress={disabled ? undefined : () => setOpen(true)}
|
|
108
|
+
accessibilityRole="button"
|
|
109
|
+
accessibilityLabel={`Country code ${selected?.dialCode ?? ''}`}
|
|
110
|
+
style={{
|
|
111
|
+
flexDirection: 'row',
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
gap: spacing.xs,
|
|
114
|
+
paddingHorizontal: spacing.sm + 2,
|
|
115
|
+
height: sizing.inputHeight,
|
|
116
|
+
borderWidth: 1,
|
|
117
|
+
borderColor: colors.border,
|
|
118
|
+
borderRadius: radius.sm,
|
|
119
|
+
backgroundColor: colors.background,
|
|
120
|
+
opacity: disabled ? 0.6 : 1,
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
<CountryFlag country={country} size={22} />
|
|
124
|
+
<MyazaText variant="body">{selected?.dialCode ?? ''}</MyazaText>
|
|
125
|
+
<Icon name="chevron-down" size={14} color={colors.textMuted} />
|
|
126
|
+
</Pressable>
|
|
127
|
+
|
|
128
|
+
<View style={{ flex: 1 }}>
|
|
129
|
+
<MyazaInput
|
|
130
|
+
value={national}
|
|
131
|
+
onChangeText={onTyped}
|
|
132
|
+
placeholder="803 123 4567"
|
|
133
|
+
keyboardType="phone-pad"
|
|
134
|
+
editable={!disabled}
|
|
135
|
+
autoFocus
|
|
136
|
+
/>
|
|
137
|
+
</View>
|
|
138
|
+
</View>
|
|
139
|
+
|
|
140
|
+
<DialCodePicker
|
|
141
|
+
visible={open}
|
|
142
|
+
options={options}
|
|
143
|
+
selected={country}
|
|
144
|
+
onPick={pickCountry}
|
|
145
|
+
onClose={() => setOpen(false)}
|
|
146
|
+
/>
|
|
147
|
+
</View>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Linking, Pressable, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { PRODUCT_URL, brandMarkColor } from '../config/brand';
|
|
5
|
+
import { spacing } from '../config/theme';
|
|
6
|
+
import { MyazaWordmark } from './MyazaWordmark';
|
|
7
|
+
import { useTheme } from './runtime';
|
|
8
|
+
import { MyazaText } from './Typography';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Vendor attribution, pinned below the step body on every screen.
|
|
12
|
+
*
|
|
13
|
+
* "POWERED BY", deliberately, not "Secured by" — at this moment we are
|
|
14
|
+
* COLLECTING a passport and a live selfie, not protecting something, so what the
|
|
15
|
+
* user needs is provenance (a name to hold responsible) rather than an
|
|
16
|
+
* unfalsifiable security promise, which is the kind of reassurance a phishing
|
|
17
|
+
* screen writes.
|
|
18
|
+
*
|
|
19
|
+
* The lockup mirrors the dashboard's canonical treatment: [Myaza wordmark],
|
|
20
|
+
* hairline rule, TRUST in tracked uppercase. The divider is a 1px VIEW, not a
|
|
21
|
+
* "|" character — a typed pipe sits on the text baseline at whatever weight the
|
|
22
|
+
* font gives it and reads as a separator between two names. The rule is what
|
|
23
|
+
* makes it one brand: Myaza Trust.
|
|
24
|
+
*
|
|
25
|
+
* Kept on the camera screens too: a trust mark that disappears exactly where
|
|
26
|
+
* biometrics are captured would vanish where it matters most.
|
|
27
|
+
*/
|
|
28
|
+
export function PoweredBy({ bottomInset = 0 }: { bottomInset?: number }): React.ReactElement {
|
|
29
|
+
const { colors } = useTheme();
|
|
30
|
+
// ONE Myaza tone for the whole mark — label, wordmark lettering, rule and
|
|
31
|
+
// TRUST — picked against the background it will actually sit on rather than
|
|
32
|
+
// taken from the org's palette. `colors.background` already reflects an org
|
|
33
|
+
// override, so this holds for a custom background too, not just light/dark.
|
|
34
|
+
const markColor = brandMarkColor(colors.background);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<View
|
|
38
|
+
style={{
|
|
39
|
+
paddingTop: spacing.md,
|
|
40
|
+
// Symmetric with the top. It was `lg`, which read as a heavy band on the
|
|
41
|
+
// one screen where vertical space is contested — the searchable country
|
|
42
|
+
// list, where the footer costs a visible row. The home-indicator inset
|
|
43
|
+
// rides on top, so notched devices still clear it comfortably.
|
|
44
|
+
paddingBottom: spacing.md + bottomInset,
|
|
45
|
+
alignItems: 'center',
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
{/* openURL can reject (no browser / a locked-down device). The mark itself
|
|
49
|
+
is the point, so a failed open is swallowed rather than surfaced. */}
|
|
50
|
+
<Pressable
|
|
51
|
+
onPress={() => {
|
|
52
|
+
void Linking.openURL(PRODUCT_URL).catch(() => undefined);
|
|
53
|
+
}}
|
|
54
|
+
accessibilityRole="link"
|
|
55
|
+
accessibilityLabel="Powered by Myaza Trust"
|
|
56
|
+
hitSlop={8}
|
|
57
|
+
style={{ flexDirection: 'row', alignItems: 'center', gap: 10, opacity: 0.9 }}
|
|
58
|
+
>
|
|
59
|
+
{/* Small and muted on purpose — "Powered by" is connective tissue, not
|
|
60
|
+
the message. The BRAND carries the weight. */}
|
|
61
|
+
<MyazaText variant="body" color={markColor} style={{ flexShrink: 1, fontSize: 12 }}>
|
|
62
|
+
Powered by
|
|
63
|
+
</MyazaText>
|
|
64
|
+
|
|
65
|
+
{/* The lockup, spaced TIGHTER than the gap before it so it reads as one
|
|
66
|
+
mark rather than three evenly-spaced items. */}
|
|
67
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: spacing.sm }}>
|
|
68
|
+
<MyazaWordmark height={28} wordmark={markColor} />
|
|
69
|
+
|
|
70
|
+
<View style={{ width: 1, height: 24, backgroundColor: markColor, opacity: 0.35 }} />
|
|
71
|
+
|
|
72
|
+
{/* ~half the wordmark's height — the ratio the dashboard lockup uses. */}
|
|
73
|
+
{/* Pinned to the brand face rather than inheriting: this word is part
|
|
74
|
+
of the MARK, and the font an org sets in their workflow would
|
|
75
|
+
otherwise redraw someone else's logo in the customer's typeface.
|
|
76
|
+
Matches the web's BRAND_FONT_STACK on the same element. */}
|
|
77
|
+
<MyazaText
|
|
78
|
+
brandMark
|
|
79
|
+
variant="body"
|
|
80
|
+
color={markColor}
|
|
81
|
+
style={{ flexShrink: 1, fontSize: 14, fontWeight: '600', letterSpacing: 2 }}
|
|
82
|
+
>
|
|
83
|
+
TRUST
|
|
84
|
+
</MyazaText>
|
|
85
|
+
</View>
|
|
86
|
+
</Pressable>
|
|
87
|
+
</View>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { Animated, Easing, ScrollView, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius, spacing } from '../config/theme';
|
|
5
|
+
import { useTheme } from './runtime';
|
|
6
|
+
import { MyazaText } from './Typography';
|
|
7
|
+
import { MyazaButton } from './MyazaButton';
|
|
8
|
+
import { Icon } from './Icon';
|
|
9
|
+
import type { ReadyContent } from './readyPrimerContent';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// "Here's what happens next" — shown once before a capture step opens the
|
|
13
|
+
// camera.
|
|
14
|
+
//
|
|
15
|
+
// It exists because the camera used to appear unannounced: a user who does not
|
|
16
|
+
// know their ID or their face is about to be photographed fumbles the first
|
|
17
|
+
// attempt, and a retake costs more than a sentence of warning.
|
|
18
|
+
//
|
|
19
|
+
// It sits BEFORE the camera-permission primer: first what we are about to do,
|
|
20
|
+
// then the OS prompt. Two asks in a row, each with a reason, never a surprise.
|
|
21
|
+
//
|
|
22
|
+
// A port of the Flutter SDK's ReadyPrimer and the web SDK's — same single-column
|
|
23
|
+
// shape (one hero, at most three expectations, one primary action) and the same
|
|
24
|
+
// copy, so the flow reads identically whichever SDK an org embeds.
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export function ReadyPrimer({
|
|
28
|
+
content,
|
|
29
|
+
onReady,
|
|
30
|
+
buttonLabel = "I'm ready",
|
|
31
|
+
}: {
|
|
32
|
+
content: ReadyContent;
|
|
33
|
+
onReady: () => void;
|
|
34
|
+
buttonLabel?: string;
|
|
35
|
+
}): React.ReactElement {
|
|
36
|
+
const { colors } = useTheme();
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
// TOP-ALIGNED, like the web and Flutter. The sheet body guarantees the
|
|
40
|
+
// child at least the viewport height, so centring parked the whole primer
|
|
41
|
+
// in the middle and left a dead band under the step indicator.
|
|
42
|
+
<ScrollView
|
|
43
|
+
contentContainerStyle={{ paddingVertical: spacing.md }}
|
|
44
|
+
showsVerticalScrollIndicator={false}
|
|
45
|
+
>
|
|
46
|
+
{/* Hero — matches the permission primer's icon treatment so the two
|
|
47
|
+
screens read as one sequence rather than two different designs. */}
|
|
48
|
+
<View
|
|
49
|
+
style={{
|
|
50
|
+
paddingHorizontal: 24,
|
|
51
|
+
paddingVertical: 36,
|
|
52
|
+
backgroundColor: `${colors.primary}0D`, // 5%
|
|
53
|
+
borderWidth: 1,
|
|
54
|
+
borderColor: `${colors.primary}26`, // 15%
|
|
55
|
+
borderRadius: radius.lg,
|
|
56
|
+
alignItems: 'center',
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<View style={{ width: 80, height: 80, alignItems: 'center', justifyContent: 'center' }}>
|
|
60
|
+
<PulseRing color={colors.primary} />
|
|
61
|
+
<View
|
|
62
|
+
style={{
|
|
63
|
+
width: 64,
|
|
64
|
+
height: 64,
|
|
65
|
+
borderRadius: 32,
|
|
66
|
+
backgroundColor: `${colors.primary}26`, // 15%
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
justifyContent: 'center',
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<Icon name={content.icon} size={28} color={colors.primary} />
|
|
72
|
+
</View>
|
|
73
|
+
</View>
|
|
74
|
+
|
|
75
|
+
<View style={{ height: spacing.md }} />
|
|
76
|
+
<MyazaText variant="heading3" style={{ textAlign: 'center' }}>
|
|
77
|
+
{content.title}
|
|
78
|
+
</MyazaText>
|
|
79
|
+
<View style={{ height: 6 }} />
|
|
80
|
+
<MyazaText variant="bodySmall" color={colors.textSecondary} style={{ textAlign: 'center' }}>
|
|
81
|
+
{content.body}
|
|
82
|
+
</MyazaText>
|
|
83
|
+
</View>
|
|
84
|
+
|
|
85
|
+
<View style={{ height: 20 }} />
|
|
86
|
+
|
|
87
|
+
{/* Expectations — the same row idiom as the consent screen's
|
|
88
|
+
"during this process we will" list. */}
|
|
89
|
+
{content.checklist.map((item) => (
|
|
90
|
+
<View key={item.label} style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12 }}>
|
|
91
|
+
<View
|
|
92
|
+
style={{
|
|
93
|
+
width: 36,
|
|
94
|
+
height: 36,
|
|
95
|
+
borderRadius: radius.xs,
|
|
96
|
+
backgroundColor: `${colors.primary}1A`, // 10%
|
|
97
|
+
alignItems: 'center',
|
|
98
|
+
justifyContent: 'center',
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<Icon name={item.icon} size={18} color={colors.primary} />
|
|
102
|
+
</View>
|
|
103
|
+
<View style={{ width: 12 }} />
|
|
104
|
+
<MyazaText variant="bodyMedium" style={{ flex: 1 }}>
|
|
105
|
+
{item.label}
|
|
106
|
+
</MyazaText>
|
|
107
|
+
</View>
|
|
108
|
+
))}
|
|
109
|
+
|
|
110
|
+
<View style={{ height: 8 }} />
|
|
111
|
+
{/* One primary action. MyazaButton already clears the 44pt minimum. */}
|
|
112
|
+
<MyazaButton label={buttonLabel} onPress={onReady} />
|
|
113
|
+
</ScrollView>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The web's `animate-pulse-ring` — a ring that expands and fades behind the glyph. */
|
|
118
|
+
function PulseRing({ color }: { color: string }): React.ReactElement {
|
|
119
|
+
const t = useRef(new Animated.Value(0)).current;
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
const loop = Animated.loop(
|
|
123
|
+
Animated.timing(t, {
|
|
124
|
+
toValue: 1,
|
|
125
|
+
duration: 2000,
|
|
126
|
+
easing: Easing.out(Easing.ease),
|
|
127
|
+
useNativeDriver: true,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
loop.start();
|
|
131
|
+
return () => loop.stop();
|
|
132
|
+
}, [t]);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<Animated.View
|
|
136
|
+
pointerEvents="none"
|
|
137
|
+
style={{
|
|
138
|
+
position: 'absolute',
|
|
139
|
+
width: 64,
|
|
140
|
+
height: 64,
|
|
141
|
+
borderRadius: 32,
|
|
142
|
+
borderWidth: 2,
|
|
143
|
+
borderColor: color,
|
|
144
|
+
opacity: t.interpolate({ inputRange: [0, 1], outputRange: [0.45, 0] }),
|
|
145
|
+
transform: [{ scale: t.interpolate({ inputRange: [0, 1], outputRange: [1, 1.35] }) }],
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { useKycStore } from './runtime';
|
|
5
|
+
import type { KYCStep } from '../types/config';
|
|
6
|
+
import { ConsentStep } from '../screens/ConsentStep';
|
|
7
|
+
import { IdTypeStep } from '../screens/IdTypeStep';
|
|
8
|
+
import { IdInputStep } from '../screens/IdInputStep';
|
|
9
|
+
import { DocumentCaptureStep } from '../screens/DocumentCaptureStep';
|
|
10
|
+
import { LivenessStep } from '../screens/LivenessStep';
|
|
11
|
+
import { SubmittedStep } from '../screens/SubmittedStep';
|
|
12
|
+
import { QuestionnaireStep } from '../screens/QuestionnaireStep';
|
|
13
|
+
import { ContactVerificationStep } from '../screens/ContactVerificationStep';
|
|
14
|
+
import { ProofOfAddressStep } from '../screens/ProofOfAddressStep';
|
|
15
|
+
import { CountrySelectStep } from '../screens/CountrySelectStep';
|
|
16
|
+
import { BusinessDetailsStep } from '../screens/BusinessDetailsStep';
|
|
17
|
+
import { BusinessKeyPeopleStep } from '../screens/BusinessKeyPeopleStep';
|
|
18
|
+
import { BusinessDocumentsStep } from '../screens/BusinessDocumentsStep';
|
|
19
|
+
import { ApplicantRoleStep } from '../screens/ApplicantRoleStep';
|
|
20
|
+
import { NfcStep } from '../screens/NfcStep';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// The step router — which screen a step renders.
|
|
24
|
+
//
|
|
25
|
+
// Split from KycFlow.tsx (200-line rule) so adding a step touches the router
|
|
26
|
+
// and the step order, and nothing else.
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
export function StepView({ step, onClose }: { step: KYCStep; onClose: () => void }): React.ReactElement {
|
|
30
|
+
switch (step) {
|
|
31
|
+
case 'consent':
|
|
32
|
+
return <ConsentStep />;
|
|
33
|
+
case 'id-type':
|
|
34
|
+
return <IdTypeStep />;
|
|
35
|
+
case 'id-input':
|
|
36
|
+
return <IdInputStep />;
|
|
37
|
+
case 'document-capture':
|
|
38
|
+
return <DocumentCaptureStep />;
|
|
39
|
+
case 'liveness':
|
|
40
|
+
return <LivenessStep />;
|
|
41
|
+
case 'questionnaire':
|
|
42
|
+
return <QuestionnaireStep />;
|
|
43
|
+
// Distinct keys are LOAD-BEARING: both mounts are the same component type
|
|
44
|
+
// at the same position, so without them React reuses the instance when the
|
|
45
|
+
// flow moves email → phone. The phone step would open holding the email
|
|
46
|
+
// step's challengeId, show its code panel, and verifying would file the
|
|
47
|
+
// EMAIL proof as the phone token — which the server then drops, blocking
|
|
48
|
+
// submission on a phone check the user appears to have passed.
|
|
49
|
+
case 'email-verification':
|
|
50
|
+
return <ContactVerificationStep key="contact-email" channel="email" />;
|
|
51
|
+
case 'phone-verification':
|
|
52
|
+
return <ContactVerificationStep key="contact-phone" channel="phone" />;
|
|
53
|
+
case 'proof-of-address':
|
|
54
|
+
return <ProofOfAddressStep />;
|
|
55
|
+
case 'country-select':
|
|
56
|
+
return <CountrySelectStep />;
|
|
57
|
+
case 'business-details':
|
|
58
|
+
return <BusinessDetailsStep />;
|
|
59
|
+
case 'business-key-people':
|
|
60
|
+
return <BusinessKeyPeopleStep />;
|
|
61
|
+
case 'business-documents':
|
|
62
|
+
return <BusinessDocumentsStep />;
|
|
63
|
+
case 'applicant-role':
|
|
64
|
+
return <ApplicantRoleStep />;
|
|
65
|
+
case 'nfc':
|
|
66
|
+
return <NfcStep />;
|
|
67
|
+
case 'submitted':
|
|
68
|
+
return <SubmittedStep onClose={onClose} />;
|
|
69
|
+
default:
|
|
70
|
+
return <UnimplementedStep step={step} />;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A step that is in the order but has no screen on this platform yet.
|
|
76
|
+
*
|
|
77
|
+
* It advances rather than rendering something — the previous `default` fell
|
|
78
|
+
* back to the consent screen, which put the user in a loop they could not leave
|
|
79
|
+
* (consent's Continue leads back to the same step). Skipping degrades an
|
|
80
|
+
* unbuilt step to "it didn't run", which is the same thing a workflow that left
|
|
81
|
+
* it off would do, and is recoverable. Logged so it surfaces in development
|
|
82
|
+
* instead of only as a step that mysteriously never appears.
|
|
83
|
+
*/
|
|
84
|
+
function UnimplementedStep({ step }: { step: KYCStep }): React.ReactElement {
|
|
85
|
+
const store = useKycStore();
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
console.warn(`[myaza-kyc] No screen for step "${step}" — skipping.`);
|
|
88
|
+
store.getState().nextStep();
|
|
89
|
+
}, [step, store]);
|
|
90
|
+
return <View />;
|
|
91
|
+
}
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { StyleSheet, Text, type TextProps, type TextStyle } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { useTheme } from './runtime';
|
|
5
|
-
import { fontFamilyFor } from './fonts';
|
|
5
|
+
import { fontFamilyFor, markFamilyName } from './fonts';
|
|
6
6
|
|
|
7
7
|
// Branded text. `variant` picks size/weight; headings render in Space Grotesk and
|
|
8
8
|
// body/labels in Karla — the SAME families the Flutter SDK uses (google_fonts) —
|
|
@@ -34,15 +34,24 @@ const HEADINGS = new Set<TextVariant>(['heading1', 'heading2', 'heading3']);
|
|
|
34
34
|
export interface MyazaTextProps extends TextProps {
|
|
35
35
|
variant?: TextVariant;
|
|
36
36
|
color?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Render in the Myaza brand face, ignoring the org's font override.
|
|
39
|
+
*
|
|
40
|
+
* For the wordmark ONLY (the "TRUST" in the footer lockup). Everything else
|
|
41
|
+
* in the flow is the customer's UI and should take the customer's typeface;
|
|
42
|
+
* a logo is not.
|
|
43
|
+
*/
|
|
44
|
+
brandMark?: boolean;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
export function MyazaText({
|
|
40
48
|
variant = 'body',
|
|
41
49
|
color,
|
|
42
50
|
style,
|
|
51
|
+
brandMark = false,
|
|
43
52
|
...rest
|
|
44
53
|
}: MyazaTextProps): React.ReactElement {
|
|
45
|
-
const { colors, fontsLoaded } = useTheme();
|
|
54
|
+
const { colors, fontsLoaded, brandFonts } = useTheme();
|
|
46
55
|
const defaultColor =
|
|
47
56
|
variant === 'bodyMedium'
|
|
48
57
|
? colors.textSecondary
|
|
@@ -54,13 +63,19 @@ export function MyazaText({
|
|
|
54
63
|
// right Space Grotesk / Karla weight (custom fonts ignore `fontWeight`, so the
|
|
55
64
|
// weight must be baked into the family).
|
|
56
65
|
const flat = (StyleSheet.flatten([VARIANTS[variant], style]) ?? {}) as TextStyle;
|
|
57
|
-
const family =
|
|
66
|
+
const family = brandMark
|
|
67
|
+
? markFamilyName(flat.fontWeight, fontsLoaded)
|
|
68
|
+
: fontFamilyFor(HEADINGS.has(variant), flat.fontWeight, fontsLoaded, brandFonts);
|
|
58
69
|
|
|
59
70
|
return (
|
|
60
71
|
<Text
|
|
61
72
|
style={[
|
|
62
73
|
flat,
|
|
63
74
|
{ color: color ?? defaultColor },
|
|
75
|
+
// EVERY resolved family — bundled or brand — encodes its weight in the
|
|
76
|
+
// family name, so fontWeight must always be cleared. Leaving it makes
|
|
77
|
+
// the platform synthesise a bolder face on top of an already-bold file:
|
|
78
|
+
// the glyphs render wider than they were measured and the text clips.
|
|
64
79
|
family ? { fontFamily: family, fontWeight: undefined } : null,
|
|
65
80
|
]}
|
|
66
81
|
{...rest}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius, spacing } from '../config/theme';
|
|
5
|
+
import { useTheme } from './runtime';
|
|
6
|
+
import { MyazaText } from './Typography';
|
|
7
|
+
import { Icon } from './Icon';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* "This is already verified" confirmation, shown when a user returns to a
|
|
11
|
+
* contact step they have already passed.
|
|
12
|
+
*
|
|
13
|
+
* A primary-tinted card rather than a bare row, matching the web and Flutter
|
|
14
|
+
* SDKs: it is the one moment in the step that reports success, and it should
|
|
15
|
+
* read as a result rather than as another line of copy.
|
|
16
|
+
*/
|
|
17
|
+
export function VerifiedNotice({ label }: { label: string }): React.ReactElement {
|
|
18
|
+
const { colors } = useTheme();
|
|
19
|
+
return (
|
|
20
|
+
<View
|
|
21
|
+
style={{
|
|
22
|
+
flexDirection: 'row',
|
|
23
|
+
alignItems: 'center',
|
|
24
|
+
gap: spacing.sm + 4,
|
|
25
|
+
padding: spacing.md,
|
|
26
|
+
borderRadius: radius.md,
|
|
27
|
+
backgroundColor: `${colors.primary}0D`,
|
|
28
|
+
borderWidth: 1,
|
|
29
|
+
borderColor: `${colors.primary}4D`,
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<Icon name="badge-check" size={20} color={colors.primary} />
|
|
33
|
+
<MyazaText variant="body" style={{ flex: 1, fontWeight: '600' }}>
|
|
34
|
+
{label}
|
|
35
|
+
</MyazaText>
|
|
36
|
+
</View>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Svg, { Path } from 'react-native-svg';
|
|
3
|
+
|
|
4
|
+
// The WhatsApp mark. Lucide dropped brand icons, and the nearest generic glyph
|
|
5
|
+
// (a speech bubble) does not read as WhatsApp — the whole point of offering the
|
|
6
|
+
// choice is that the user recognises the channel at a glance.
|
|
7
|
+
//
|
|
8
|
+
// Path data: Simple Icons (CC0), 24×24 viewBox. Byte-identical to the web and
|
|
9
|
+
// Flutter SDKs so the tile looks the same everywhere.
|
|
10
|
+
//
|
|
11
|
+
// Tinted by `color` rather than fixed brand green: it sits in a tile whose icon
|
|
12
|
+
// takes the selected/unselected treatment, and a fixed green would fight that
|
|
13
|
+
// (and the dark theme). Nominative use of the mark to label the channel.
|
|
14
|
+
const WHATSAPP_PATH =
|
|
15
|
+
'M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.872.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z';
|
|
16
|
+
|
|
17
|
+
export function WhatsAppIcon({
|
|
18
|
+
size = 16,
|
|
19
|
+
color,
|
|
20
|
+
}: {
|
|
21
|
+
size?: number;
|
|
22
|
+
color: string;
|
|
23
|
+
}): React.ReactElement {
|
|
24
|
+
return (
|
|
25
|
+
<Svg width={size} height={size} viewBox="0 0 24 24">
|
|
26
|
+
<Path d={WHATSAPP_PATH} fill={color} />
|
|
27
|
+
</Svg>
|
|
28
|
+
);
|
|
29
|
+
}
|