@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,127 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ActivityIndicator, Pressable, View } from 'react-native';
|
|
3
|
+
import Svg, { Circle } from 'react-native-svg';
|
|
4
|
+
|
|
5
|
+
import { radius, spacing } from '../../config/theme';
|
|
6
|
+
import { useTheme } from '../runtime';
|
|
7
|
+
import { MyazaText } from '../Typography';
|
|
8
|
+
import { Icon } from '../Icon';
|
|
9
|
+
import { CountryFlag } from '../CountryFlag';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Immersive capture chrome — 1:1 with the Flutter SDK's full-screen camera.
|
|
13
|
+
//
|
|
14
|
+
// Positions are deliberate and differ from the framed viewfinder:
|
|
15
|
+
// • the HINT sits at the BOTTOM, near the shutter the user is about to press,
|
|
16
|
+
// not at the top where it competes with the document itself;
|
|
17
|
+
// • the TORCH is bottom-right, in thumb reach on a phone held two-handed;
|
|
18
|
+
// • the side badge and document pill sit top, out of the frame's way.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const SCRIM = 'rgba(0,0,0,0.55)';
|
|
22
|
+
|
|
23
|
+
/** Round translucent control — back, torch. */
|
|
24
|
+
export function RoundControl({
|
|
25
|
+
icon,
|
|
26
|
+
label,
|
|
27
|
+
onPress,
|
|
28
|
+
active,
|
|
29
|
+
size = 48,
|
|
30
|
+
style,
|
|
31
|
+
}: {
|
|
32
|
+
icon: 'back' | 'lightbulb';
|
|
33
|
+
label: string;
|
|
34
|
+
onPress: () => void;
|
|
35
|
+
active?: boolean;
|
|
36
|
+
size?: number;
|
|
37
|
+
style: object;
|
|
38
|
+
}): React.ReactElement {
|
|
39
|
+
const { colors } = useTheme();
|
|
40
|
+
return (
|
|
41
|
+
<Pressable
|
|
42
|
+
onPress={onPress}
|
|
43
|
+
accessibilityRole="button"
|
|
44
|
+
accessibilityLabel={label}
|
|
45
|
+
style={{
|
|
46
|
+
position: 'absolute',
|
|
47
|
+
width: size,
|
|
48
|
+
height: size,
|
|
49
|
+
borderRadius: radius.full,
|
|
50
|
+
backgroundColor: active ? colors.primary : SCRIM,
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
justifyContent: 'center',
|
|
53
|
+
...style,
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
<Icon name={icon} size={20} color="#FFFFFF" />
|
|
57
|
+
</Pressable>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** "FRONT" / "BACK" — which face of the document is expected right now. */
|
|
62
|
+
export function SideBadge({ side, top }: { side: 'front' | 'back'; top: number }): React.ReactElement {
|
|
63
|
+
const { colors } = useTheme();
|
|
64
|
+
return (
|
|
65
|
+
<View
|
|
66
|
+
style={{
|
|
67
|
+
position: 'absolute',
|
|
68
|
+
top,
|
|
69
|
+
right: spacing.md,
|
|
70
|
+
backgroundColor: colors.primary,
|
|
71
|
+
borderRadius: radius.full,
|
|
72
|
+
paddingHorizontal: 16,
|
|
73
|
+
paddingVertical: 8,
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<MyazaText variant="bodySmall" color="#FFFFFF" style={{ fontWeight: '800', letterSpacing: 0.8 }}>
|
|
77
|
+
{side === 'back' ? 'BACK' : 'FRONT'}
|
|
78
|
+
</MyazaText>
|
|
79
|
+
</View>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Flag + document name, so the user can see WHICH document is expected. */
|
|
84
|
+
export function DocumentPill({
|
|
85
|
+
country,
|
|
86
|
+
label,
|
|
87
|
+
top,
|
|
88
|
+
}: {
|
|
89
|
+
country: string;
|
|
90
|
+
label: string;
|
|
91
|
+
top: number;
|
|
92
|
+
}): React.ReactElement {
|
|
93
|
+
return (
|
|
94
|
+
<View style={{ position: 'absolute', top, left: 0, right: 0, alignItems: 'center' }}>
|
|
95
|
+
<View
|
|
96
|
+
style={{
|
|
97
|
+
flexDirection: 'row',
|
|
98
|
+
alignItems: 'center',
|
|
99
|
+
backgroundColor: SCRIM,
|
|
100
|
+
borderRadius: radius.full,
|
|
101
|
+
borderWidth: 1,
|
|
102
|
+
borderColor: 'rgba(255,255,255,0.18)',
|
|
103
|
+
paddingHorizontal: 12,
|
|
104
|
+
paddingVertical: 7,
|
|
105
|
+
maxWidth: '80%',
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
{/* The flag carries the country on its own — a name beside it is the
|
|
109
|
+
same fact twice, and it reads faster at a glance than text. */}
|
|
110
|
+
{country ? (
|
|
111
|
+
<>
|
|
112
|
+
<CountryFlag country={country} size={26} />
|
|
113
|
+
<View style={{ width: 9 }} />
|
|
114
|
+
</>
|
|
115
|
+
) : null}
|
|
116
|
+
<MyazaText
|
|
117
|
+
variant="bodyMedium"
|
|
118
|
+
color="#FFFFFF"
|
|
119
|
+
numberOfLines={1}
|
|
120
|
+
style={{ fontWeight: '700', flexShrink: 1 }}
|
|
121
|
+
>
|
|
122
|
+
{label}
|
|
123
|
+
</MyazaText>
|
|
124
|
+
</View>
|
|
125
|
+
</View>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { Animated, Easing } from 'react-native';
|
|
3
|
+
import Svg, { Defs, G, Line, Mask, Path, Rect } from 'react-native-svg';
|
|
4
|
+
|
|
5
|
+
import { useTheme } from '../runtime';
|
|
6
|
+
import { DocumentGhost } from './DocumentGhost';
|
|
7
|
+
import type { DocumentFraming } from '../../capture';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// The full-screen document guide.
|
|
11
|
+
//
|
|
12
|
+
// A 1:1 port of the Flutter SDK's immersive viewfinder frame: a dimmed
|
|
13
|
+
// surround, a rounded window at the document's own aspect, heavy corner
|
|
14
|
+
// brackets, a faint grid inside, and a scan line sweeping down it.
|
|
15
|
+
//
|
|
16
|
+
// The grid and the sweep are not decoration — they are what makes a dim,
|
|
17
|
+
// featureless preview read as "actively looking at something". Without them a
|
|
18
|
+
// user holding a passport still gets no feedback until the shutter fires.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/** Share of the screen width the guide occupies (Flutter uses ~88%). */
|
|
22
|
+
const GUIDE_WIDTH_FRACTION = 0.88;
|
|
23
|
+
/** Grid divisions inside the window. */
|
|
24
|
+
const GRID_COLS = 8;
|
|
25
|
+
const GRID_ROWS = 6;
|
|
26
|
+
|
|
27
|
+
export function ImmersiveGuide({
|
|
28
|
+
guideAspect,
|
|
29
|
+
primary,
|
|
30
|
+
width,
|
|
31
|
+
height,
|
|
32
|
+
framing,
|
|
33
|
+
showMrzBand,
|
|
34
|
+
}: {
|
|
35
|
+
guideAspect: number;
|
|
36
|
+
primary: string;
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
/** Auto-capture's verdict for the current frame. */
|
|
40
|
+
framing: DocumentFraming;
|
|
41
|
+
/** Draw the ghost's machine-readable band (passport data pages carry one). */
|
|
42
|
+
showMrzBand: boolean;
|
|
43
|
+
}): React.ReactElement {
|
|
44
|
+
const { colors } = useTheme();
|
|
45
|
+
|
|
46
|
+
// Each state answers a question the user is actually asking:
|
|
47
|
+
// searching → "is the camera even working?" → neutral, sweeping
|
|
48
|
+
// wrongShape/adjust → "what do I do?" → amber, motion stops
|
|
49
|
+
// holding/ready → "did it see it?" → green, locked
|
|
50
|
+
//
|
|
51
|
+
// Amber for "I can see something but it isn't right" is distinct from the
|
|
52
|
+
// neutral searching state on purpose: it tells the user to ACT rather than
|
|
53
|
+
// wait, which is the difference between a fixable framing error and a camera
|
|
54
|
+
// that looks frozen.
|
|
55
|
+
const locked = framing === 'holding' || framing === 'ready';
|
|
56
|
+
const needsAction = framing === 'wrongShape' || framing === 'adjust';
|
|
57
|
+
const accent = locked ? colors.success : needsAction ? colors.warning : primary;
|
|
58
|
+
|
|
59
|
+
// Only dim once confident: dimming while still searching makes a dark room
|
|
60
|
+
// impossible to aim in.
|
|
61
|
+
const scrim = locked ? 0.45 : needsAction ? 0.25 : 0.18;
|
|
62
|
+
const gw = width * GUIDE_WIDTH_FRACTION;
|
|
63
|
+
const gh = gw / guideAspect;
|
|
64
|
+
const gx = (width - gw) / 2;
|
|
65
|
+
const gy = (height - gh) / 2;
|
|
66
|
+
const corner = Math.min(gw, gh) * 0.16;
|
|
67
|
+
const r = 14;
|
|
68
|
+
|
|
69
|
+
// The sweep runs the height of the window and repeats. Native driver: it is a
|
|
70
|
+
// pure transform, and a JS-driven animation over a live camera preview
|
|
71
|
+
// stutters exactly when the user is trying to hold still.
|
|
72
|
+
const sweep = useRef(new Animated.Value(0)).current;
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const loop = Animated.loop(
|
|
75
|
+
Animated.timing(sweep, {
|
|
76
|
+
toValue: 1,
|
|
77
|
+
duration: 2600,
|
|
78
|
+
easing: Easing.inOut(Easing.ease),
|
|
79
|
+
useNativeDriver: true,
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
if (locked) {
|
|
83
|
+
// Freeze once locked: continuing to sweep would imply we are still
|
|
84
|
+
// looking after we have decided.
|
|
85
|
+
sweep.stopAnimation();
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
loop.start();
|
|
89
|
+
return () => loop.stop();
|
|
90
|
+
}, [sweep, locked]);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
<Svg style={{ position: 'absolute', inset: 0 }} width={width} height={height}>
|
|
95
|
+
<Defs>
|
|
96
|
+
<Mask id="guideWindow">
|
|
97
|
+
<Rect width={width} height={height} fill="white" />
|
|
98
|
+
<Rect x={gx} y={gy} width={gw} height={gh} rx={r} fill="black" />
|
|
99
|
+
</Mask>
|
|
100
|
+
</Defs>
|
|
101
|
+
|
|
102
|
+
{/* Everything outside the window is dimmed, so the document reads as
|
|
103
|
+
the lit subject even on a dark desk. */}
|
|
104
|
+
<Rect width={width} height={height} fill={`rgba(0,0,0,${scrim})`} mask="url(#guideWindow)" />
|
|
105
|
+
|
|
106
|
+
{/* Faint grid — alignment help that survives a low-contrast page. */}
|
|
107
|
+
<G opacity={0.28}>
|
|
108
|
+
{Array.from({ length: GRID_COLS - 1 }, (_, i) => (
|
|
109
|
+
<Line
|
|
110
|
+
key={`c${i}`}
|
|
111
|
+
x1={gx + (gw / GRID_COLS) * (i + 1)}
|
|
112
|
+
y1={gy}
|
|
113
|
+
x2={gx + (gw / GRID_COLS) * (i + 1)}
|
|
114
|
+
y2={gy + gh}
|
|
115
|
+
stroke={accent}
|
|
116
|
+
strokeWidth={0.75}
|
|
117
|
+
/>
|
|
118
|
+
))}
|
|
119
|
+
{Array.from({ length: GRID_ROWS - 1 }, (_, i) => (
|
|
120
|
+
<Line
|
|
121
|
+
key={`r${i}`}
|
|
122
|
+
x1={gx}
|
|
123
|
+
y1={gy + (gh / GRID_ROWS) * (i + 1)}
|
|
124
|
+
x2={gx + gw}
|
|
125
|
+
y2={gy + (gh / GRID_ROWS) * (i + 1)}
|
|
126
|
+
stroke={accent}
|
|
127
|
+
strokeWidth={0.75}
|
|
128
|
+
/>
|
|
129
|
+
))}
|
|
130
|
+
</G>
|
|
131
|
+
|
|
132
|
+
<Rect x={gx} y={gy} width={gw} height={gh} rx={r} fill="none" stroke={`${accent}66`} strokeWidth={1.5} />
|
|
133
|
+
|
|
134
|
+
{/* Corner brackets — the frame's actual edges, heavy enough to aim by. */}
|
|
135
|
+
{[
|
|
136
|
+
`M${gx},${gy + corner} L${gx},${gy + r} Q${gx},${gy} ${gx + r},${gy} L${gx + corner},${gy}`,
|
|
137
|
+
`M${gx + gw - corner},${gy} L${gx + gw - r},${gy} Q${gx + gw},${gy} ${gx + gw},${gy + r} L${gx + gw},${gy + corner}`,
|
|
138
|
+
`M${gx + gw},${gy + gh - corner} L${gx + gw},${gy + gh - r} Q${gx + gw},${gy + gh} ${gx + gw - r},${gy + gh} L${gx + gw - corner},${gy + gh}`,
|
|
139
|
+
`M${gx + corner},${gy + gh} L${gx + r},${gy + gh} Q${gx},${gy + gh} ${gx},${gy + gh - r} L${gx},${gy + gh - corner}`,
|
|
140
|
+
].map((d, i) => (
|
|
141
|
+
<Path key={i} d={d} stroke={accent} strokeWidth={4} fill="none" strokeLinecap="round" />
|
|
142
|
+
))}
|
|
143
|
+
</Svg>
|
|
144
|
+
|
|
145
|
+
{/* Scan line, above the SVG so it can be transform-animated natively. */}
|
|
146
|
+
<Animated.View
|
|
147
|
+
pointerEvents="none"
|
|
148
|
+
style={{
|
|
149
|
+
position: 'absolute',
|
|
150
|
+
left: gx,
|
|
151
|
+
top: gy,
|
|
152
|
+
width: gw,
|
|
153
|
+
height: 2,
|
|
154
|
+
backgroundColor: accent,
|
|
155
|
+
shadowColor: accent,
|
|
156
|
+
shadowOpacity: 0.9,
|
|
157
|
+
shadowRadius: 8,
|
|
158
|
+
shadowOffset: { width: 0, height: 0 },
|
|
159
|
+
transform: [
|
|
160
|
+
{ translateY: sweep.interpolate({ inputRange: [0, 1], outputRange: [0, gh - 2] }) },
|
|
161
|
+
],
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
164
|
+
<DocumentGhost
|
|
165
|
+
x={gx}
|
|
166
|
+
y={gy}
|
|
167
|
+
width={gw}
|
|
168
|
+
height={gh}
|
|
169
|
+
showMrzBand={showMrzBand}
|
|
170
|
+
documentFound={framing !== 'none' && framing !== 'wrongShape'}
|
|
171
|
+
/>
|
|
172
|
+
</>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../../config/theme';
|
|
5
|
+
import { useTheme } from '../runtime';
|
|
6
|
+
import { ImmersiveGuide } from './ImmersiveGuide';
|
|
7
|
+
import { DocumentPill, RoundControl, SideBadge } from './ImmersiveControls';
|
|
8
|
+
import { BottomBar } from './ImmersiveBottomBar';
|
|
9
|
+
import type { DocumentFraming } from '../../capture';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Everything drawn over the full-screen camera.
|
|
13
|
+
*
|
|
14
|
+
* Composed here rather than in CameraViewfinder so that file stays about camera
|
|
15
|
+
* wiring — and so the immersive layout can be read in one place against the
|
|
16
|
+
* Flutter screen it mirrors.
|
|
17
|
+
*/
|
|
18
|
+
export function ImmersiveOverlay(p: {
|
|
19
|
+
side: 'front' | 'back';
|
|
20
|
+
country: string;
|
|
21
|
+
documentLabel: string;
|
|
22
|
+
guideAspect: number;
|
|
23
|
+
hint: string;
|
|
24
|
+
hintIsAction: boolean;
|
|
25
|
+
progress: number;
|
|
26
|
+
busy: boolean;
|
|
27
|
+
torch: boolean;
|
|
28
|
+
hasTorch: boolean;
|
|
29
|
+
onToggleTorch: () => void;
|
|
30
|
+
onCapture: () => void;
|
|
31
|
+
onBack: (() => void) | null;
|
|
32
|
+
onUpload: (() => void) | null;
|
|
33
|
+
bottomInset: number;
|
|
34
|
+
width: number;
|
|
35
|
+
height: number;
|
|
36
|
+
topInset: number;
|
|
37
|
+
framing: DocumentFraming;
|
|
38
|
+
/** Passports carry an MRZ band; the ghost draws it so it is not cropped. */
|
|
39
|
+
showMrzBand: boolean;
|
|
40
|
+
}): React.ReactElement {
|
|
41
|
+
const { colors } = useTheme();
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
<ImmersiveGuide
|
|
45
|
+
guideAspect={p.guideAspect}
|
|
46
|
+
primary={colors.primary}
|
|
47
|
+
width={p.width}
|
|
48
|
+
height={p.height}
|
|
49
|
+
framing={p.framing}
|
|
50
|
+
showMrzBand={p.showMrzBand}
|
|
51
|
+
/>
|
|
52
|
+
{p.onBack ? (
|
|
53
|
+
<RoundControl
|
|
54
|
+
icon="back"
|
|
55
|
+
label="Go back"
|
|
56
|
+
onPress={p.onBack}
|
|
57
|
+
style={{ top: p.topInset, left: spacing.md }}
|
|
58
|
+
/>
|
|
59
|
+
) : null}
|
|
60
|
+
<SideBadge side={p.side} top={p.topInset + 4} />
|
|
61
|
+
<DocumentPill country={p.country} label={p.documentLabel} top={p.topInset + 62} />
|
|
62
|
+
<BottomBar
|
|
63
|
+
hint={p.hint}
|
|
64
|
+
hintIsAction={p.hintIsAction}
|
|
65
|
+
allowUpload={!!p.onUpload}
|
|
66
|
+
onUpload={() => p.onUpload?.()}
|
|
67
|
+
onCapture={p.onCapture}
|
|
68
|
+
busy={p.busy}
|
|
69
|
+
ready
|
|
70
|
+
progress={p.progress}
|
|
71
|
+
hasTorch={p.hasTorch}
|
|
72
|
+
torch={p.torch}
|
|
73
|
+
onToggleTorch={p.onToggleTorch}
|
|
74
|
+
bottomInset={p.bottomInset}
|
|
75
|
+
/>
|
|
76
|
+
<View pointerEvents="none" />
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ActivityIndicator, Pressable, View } from 'react-native';
|
|
3
|
+
import Svg, { Circle } from 'react-native-svg';
|
|
4
|
+
|
|
5
|
+
import { radius, spacing } from '../../config/theme';
|
|
6
|
+
import { useTheme } from '../runtime';
|
|
7
|
+
import { MyazaText } from '../Typography';
|
|
8
|
+
import { Icon } from '../Icon';
|
|
9
|
+
|
|
10
|
+
/** Top offset that clears the status bar when the camera runs full-bleed. */
|
|
11
|
+
export function topInset(fill: boolean): number {
|
|
12
|
+
return fill ? spacing.xl + spacing.md : spacing.md;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The live instruction from the auto-capture gate.
|
|
17
|
+
*
|
|
18
|
+
* It replaced a static "align your ID" precisely because that told the user
|
|
19
|
+
* nothing about WHICH way to move — and the commonest failure, holding the
|
|
20
|
+
* document too close, is the one nobody guesses. Action hints take the primary
|
|
21
|
+
* colour so they read as something to do.
|
|
22
|
+
*/
|
|
23
|
+
export function HintBanner({
|
|
24
|
+
text,
|
|
25
|
+
isAction,
|
|
26
|
+
fill,
|
|
27
|
+
}: {
|
|
28
|
+
text: string;
|
|
29
|
+
isAction: boolean;
|
|
30
|
+
fill: boolean;
|
|
31
|
+
}): React.ReactElement {
|
|
32
|
+
const { colors } = useTheme();
|
|
33
|
+
return (
|
|
34
|
+
<View style={{ position: 'absolute', top: topInset(fill), left: 0, right: 0, alignItems: 'center' }}>
|
|
35
|
+
<View
|
|
36
|
+
style={{
|
|
37
|
+
backgroundColor: isAction ? colors.primary : 'rgba(0,0,0,0.4)',
|
|
38
|
+
paddingHorizontal: spacing.sm + 4,
|
|
39
|
+
paddingVertical: 6,
|
|
40
|
+
borderRadius: radius.full,
|
|
41
|
+
maxWidth: '90%',
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<MyazaText variant="bodySmall" color="#FFFFFF" style={{ textAlign: 'center' }}>
|
|
45
|
+
{text}
|
|
46
|
+
</MyazaText>
|
|
47
|
+
</View>
|
|
48
|
+
</View>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A round overlay button — the torch and the immersive back control. */
|
|
53
|
+
export function OverlayButton({
|
|
54
|
+
icon,
|
|
55
|
+
onPress,
|
|
56
|
+
label,
|
|
57
|
+
active,
|
|
58
|
+
side,
|
|
59
|
+
fill,
|
|
60
|
+
}: {
|
|
61
|
+
icon: 'lightbulb' | 'back';
|
|
62
|
+
onPress: () => void;
|
|
63
|
+
label: string;
|
|
64
|
+
active?: boolean;
|
|
65
|
+
side: 'left' | 'right';
|
|
66
|
+
fill: boolean;
|
|
67
|
+
}): React.ReactElement {
|
|
68
|
+
const { colors } = useTheme();
|
|
69
|
+
return (
|
|
70
|
+
<Pressable
|
|
71
|
+
onPress={onPress}
|
|
72
|
+
accessibilityRole="button"
|
|
73
|
+
accessibilityLabel={label}
|
|
74
|
+
style={{
|
|
75
|
+
position: 'absolute',
|
|
76
|
+
top: topInset(fill),
|
|
77
|
+
[side]: spacing.md,
|
|
78
|
+
width: 36,
|
|
79
|
+
height: 36,
|
|
80
|
+
borderRadius: radius.full,
|
|
81
|
+
backgroundColor: active ? colors.primary : 'rgba(0,0,0,0.4)',
|
|
82
|
+
alignItems: 'center',
|
|
83
|
+
justifyContent: 'center',
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<Icon name={icon} size={18} color="#FFFFFF" />
|
|
87
|
+
</Pressable>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The shutter, with the stability dwell drawn around it.
|
|
93
|
+
*
|
|
94
|
+
* The ring is what tells a user who is holding steady that something is
|
|
95
|
+
* happening — without it, auto-capture looks like a camera doing nothing right
|
|
96
|
+
* up until it fires.
|
|
97
|
+
*/
|
|
98
|
+
export function Shutter({
|
|
99
|
+
onPress,
|
|
100
|
+
ready,
|
|
101
|
+
busy,
|
|
102
|
+
progress,
|
|
103
|
+
}: {
|
|
104
|
+
onPress: () => void;
|
|
105
|
+
ready: boolean;
|
|
106
|
+
busy: boolean;
|
|
107
|
+
progress: number;
|
|
108
|
+
}): React.ReactElement {
|
|
109
|
+
const { colors } = useTheme();
|
|
110
|
+
const R = 36;
|
|
111
|
+
const CIRC = 2 * Math.PI * R;
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<View style={{ position: 'absolute', bottom: spacing.lg, left: 0, right: 0, alignItems: 'center' }}>
|
|
115
|
+
{progress > 0 && progress < 1 ? (
|
|
116
|
+
<Svg width={80} height={80} style={{ position: 'absolute', bottom: -8 }}>
|
|
117
|
+
<Circle cx={40} cy={40} r={R} stroke="rgba(255,255,255,0.25)" strokeWidth={3} fill="none" />
|
|
118
|
+
<Circle
|
|
119
|
+
cx={40}
|
|
120
|
+
cy={40}
|
|
121
|
+
r={R}
|
|
122
|
+
stroke={colors.primary}
|
|
123
|
+
strokeWidth={3}
|
|
124
|
+
fill="none"
|
|
125
|
+
strokeLinecap="round"
|
|
126
|
+
strokeDasharray={CIRC}
|
|
127
|
+
strokeDashoffset={CIRC * (1 - progress)}
|
|
128
|
+
transform="rotate(-90 40 40)"
|
|
129
|
+
/>
|
|
130
|
+
</Svg>
|
|
131
|
+
) : null}
|
|
132
|
+
<Pressable
|
|
133
|
+
onPress={onPress}
|
|
134
|
+
disabled={busy || !ready}
|
|
135
|
+
accessibilityRole="button"
|
|
136
|
+
accessibilityLabel="Capture photo"
|
|
137
|
+
style={({ pressed }) => ({
|
|
138
|
+
width: 64,
|
|
139
|
+
height: 64,
|
|
140
|
+
borderRadius: radius.full,
|
|
141
|
+
backgroundColor: ready ? '#FFFFFF' : colors.gray400,
|
|
142
|
+
borderWidth: 4,
|
|
143
|
+
borderColor: ready ? colors.primary : colors.gray300,
|
|
144
|
+
alignItems: 'center',
|
|
145
|
+
justifyContent: 'center',
|
|
146
|
+
opacity: busy ? 0.6 : 1,
|
|
147
|
+
transform: [{ scale: pressed ? 0.94 : 1 }],
|
|
148
|
+
})}
|
|
149
|
+
>
|
|
150
|
+
{busy ? <ActivityIndicator color={colors.primary} /> : <Icon name="camera" size={24} color={ready ? colors.primary : '#FFFFFF'} />}
|
|
151
|
+
</Pressable>
|
|
152
|
+
</View>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendor brand constants for the SDK's own attribution — NOT the integrating
|
|
3
|
+
* org's branding (that is `useBranding()`, driven by `/api/kyc/config`).
|
|
4
|
+
*
|
|
5
|
+
* The platform is **Myaza Trust**, a product of **Myaza** (the parent company).
|
|
6
|
+
* Always the full product name: bare "Myaza" is the company, and a pipe
|
|
7
|
+
* ("Myaza | Trust") reads as a separator between two items rather than one
|
|
8
|
+
* name. See the naming rule in kyc-dashboard/CLAUDE.md.
|
|
9
|
+
*/
|
|
10
|
+
export const PRODUCT_NAME = 'Myaza Trust';
|
|
11
|
+
|
|
12
|
+
/** The product site — where "who are these people?" gets answered. */
|
|
13
|
+
export const PRODUCT_URL = 'https://trust.myaza.co';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Wordmark ink. The icon keeps its brand colours in both themes; only the
|
|
17
|
+
* "myaza" lettering flips. These are the exact values the dashboard's own Logo
|
|
18
|
+
* renders (logo.svg / logo-white.svg), so the mark is brand-accurate rather
|
|
19
|
+
* than tinted to whatever the sheet's text colour happens to be.
|
|
20
|
+
*/
|
|
21
|
+
export const WORDMARK_LIGHT = '#19156F';
|
|
22
|
+
export const WORDMARK_DARK = '#F6F5FE';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* End-user legal documents linked from the consent screen. These are MYAZA's
|
|
26
|
+
* terms — the person consents to us processing their data as the verification
|
|
27
|
+
* provider — so they are never org-overridable.
|
|
28
|
+
*/
|
|
29
|
+
export const TERMS_URL = 'https://trust.myaza.co/legal/terms';
|
|
30
|
+
export const PRIVACY_URL = 'https://trust.myaza.co/legal/privacy';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Version of the consent wording. Consent is given by ACTING (tapping
|
|
34
|
+
* Continue) rather than ticking a box, so bump this whenever the copy changes
|
|
35
|
+
* materially and store it with the verification — otherwise nothing proves
|
|
36
|
+
* which disclosure was on screen. Keep in step with the web SDK's value.
|
|
37
|
+
*/
|
|
38
|
+
export const CONSENT_VERSION = '2026-08-06.1';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Tones the footer mark may use — the design system's INK and LIGHT text
|
|
42
|
+
* colours, not the brand purple.
|
|
43
|
+
*
|
|
44
|
+
* Attribution, not advertising. A saturated purple mark is the loudest thing in
|
|
45
|
+
* the footer on an org whose palette is yellow or green: it draws the eye to the
|
|
46
|
+
* least important element on screen and reads as a clash rather than a
|
|
47
|
+
* signature. Every comparable vendor mark (Stripe, Plaid, Persona) is monochrome
|
|
48
|
+
* for the same reason.
|
|
49
|
+
*
|
|
50
|
+
* Nothing is lost by it — the logo ICON keeps its fixed brand fills, so the
|
|
51
|
+
* Myaza identity is still carried by the mark itself while the text recedes.
|
|
52
|
+
* These two are the design system's own text tones, so this stays on-brand
|
|
53
|
+
* rather than arbitrary black-on-white.
|
|
54
|
+
*
|
|
55
|
+
* Mirrors the web SDK's `lib/brand.ts` — keep the list and the rule in step.
|
|
56
|
+
*/
|
|
57
|
+
const MARK_TONES = ['#070330', '#F6F5FE'] as const;
|
|
58
|
+
|
|
59
|
+
/** WCAG AA for the small text this renders at. */
|
|
60
|
+
const MIN_MARK_CONTRAST = 4.5;
|
|
61
|
+
|
|
62
|
+
function relativeLuminance(hex: string): number {
|
|
63
|
+
const h = hex.replace('#', '');
|
|
64
|
+
const channel = (i: number): number => {
|
|
65
|
+
const s = parseInt(h.slice(i, i + 2), 16) / 255;
|
|
66
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
67
|
+
};
|
|
68
|
+
return 0.2126 * channel(0) + 0.7152 * channel(2) + 0.0722 * channel(4);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function contrastRatio(a: string, b: string): number {
|
|
72
|
+
const la = relativeLuminance(a);
|
|
73
|
+
const lb = relativeLuminance(b);
|
|
74
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The mark tone that stays legible on `background`.
|
|
79
|
+
*
|
|
80
|
+
* The footer mark must NOT follow the org's palette: it is our attribution, and
|
|
81
|
+
* an org whose brand is close to ours would otherwise get a mark that either
|
|
82
|
+
* stops reading as Myaza or disappears into the background entirely. Chosen by
|
|
83
|
+
* CONTRAST rather than by light/dark, because an org can set any background —
|
|
84
|
+
* including mid-tones where neither variant is obviously right.
|
|
85
|
+
*
|
|
86
|
+
* Falls back to the most visible tone when none clears AA, so the answer is
|
|
87
|
+
* always the best available rather than a fixed guess.
|
|
88
|
+
*/
|
|
89
|
+
export function brandMarkColor(background: string): string {
|
|
90
|
+
const bg = /^#[0-9a-f]{6}$/i.test(background.trim()) ? background.trim() : '#FFFFFF';
|
|
91
|
+
for (const tone of MARK_TONES) {
|
|
92
|
+
if (contrastRatio(tone, bg) >= MIN_MARK_CONTRAST) return tone;
|
|
93
|
+
}
|
|
94
|
+
return MARK_TONES.reduce((best, tone) =>
|
|
95
|
+
contrastRatio(tone, bg) > contrastRatio(best, bg) ? tone : best,
|
|
96
|
+
);
|
|
97
|
+
}
|