@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,89 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo, useState } from 'react';
|
|
2
|
+
import { useColorScheme } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { resolveColors, type MyazaColorScheme, type ThemeMode , applyRadiusScale } from '../config/theme';
|
|
5
|
+
import type { KYCAppearance } from '../types/config';
|
|
6
|
+
import { useMyazaFonts } from './fonts';
|
|
7
|
+
import { useBrandFonts } from './brand-font';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Theme context.
|
|
11
|
+
//
|
|
12
|
+
// Split from the runtime provider because theming and the flow have different
|
|
13
|
+
// lifetimes: the trigger button and the workflow-resolution barrier both need
|
|
14
|
+
// colours BEFORE there is a resolved config to build a store from. Nesting is
|
|
15
|
+
// supported and the inner provider wins, which is how a resolved workflow's
|
|
16
|
+
// appearance takes over from the props' once it lands.
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export interface ThemeValue {
|
|
20
|
+
mode: ThemeMode;
|
|
21
|
+
colors: MyazaColorScheme;
|
|
22
|
+
setMode: (mode: ThemeMode) => void;
|
|
23
|
+
toggle: () => void;
|
|
24
|
+
/** True once Space Grotesk + Karla are loaded (system font until then). */
|
|
25
|
+
fontsLoaded: boolean;
|
|
26
|
+
/** Org font families from `appearance`, when set. Empty object otherwise. */
|
|
27
|
+
brandFonts: { body?: string; heading?: string };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ThemeContext = createContext<ThemeValue | null>(null);
|
|
31
|
+
|
|
32
|
+
export function MyazaThemeProvider({
|
|
33
|
+
appearance,
|
|
34
|
+
children,
|
|
35
|
+
}: {
|
|
36
|
+
appearance?: KYCAppearance;
|
|
37
|
+
children: React.ReactNode;
|
|
38
|
+
}): React.ReactElement {
|
|
39
|
+
// `theme: 'system'` follows the DEVICE's colour scheme — live, so an
|
|
40
|
+
// OS-level switch mid-flow repaints — until the in-flow toggle picks a mode
|
|
41
|
+
// explicitly (`override`), which then wins for the rest of the session.
|
|
42
|
+
// 'light'/'dark' (and unset → light) pin the mode exactly as before.
|
|
43
|
+
const systemScheme = useColorScheme();
|
|
44
|
+
const configured = appearance?.theme;
|
|
45
|
+
const [override, setOverride] = useState<ThemeMode | null>(
|
|
46
|
+
configured === 'system' ? null : configured ?? 'light',
|
|
47
|
+
);
|
|
48
|
+
const mode: ThemeMode = override ?? (systemScheme === 'dark' ? 'dark' : 'light');
|
|
49
|
+
const setMode = setOverride as (mode: ThemeMode) => void;
|
|
50
|
+
const colors = useMemo(() => resolveColors(mode, appearance), [mode, appearance]);
|
|
51
|
+
const fontsLoaded = useMyazaFonts();
|
|
52
|
+
|
|
53
|
+
// Applied here rather than at launch so a nested provider (a resolved
|
|
54
|
+
// workflow's appearance taking over from the props') re-scales too. The
|
|
55
|
+
// radius scale is a module-level object read during render, so setting it
|
|
56
|
+
// while rendering this provider lands before any child reads it.
|
|
57
|
+
applyRadiusScale(appearance?.borderRadius);
|
|
58
|
+
|
|
59
|
+
// Only families that are actually REGISTERED come back. Applying a name RN
|
|
60
|
+
// can't resolve is what made a configured font silently fall back to system —
|
|
61
|
+
// the hook loads it first, then re-renders with it.
|
|
62
|
+
const brandFonts = useBrandFonts(appearance?.fontFamily, appearance?.headingFontFamily);
|
|
63
|
+
|
|
64
|
+
const value = useMemo<ThemeValue>(
|
|
65
|
+
() => ({
|
|
66
|
+
mode,
|
|
67
|
+
colors,
|
|
68
|
+
setMode,
|
|
69
|
+
// Flip from the EFFECTIVE mode (which may be system-derived), so the
|
|
70
|
+
// first toggle out of system mode lands on the opposite of what the
|
|
71
|
+
// user is currently seeing.
|
|
72
|
+
toggle: () => setOverride(mode === 'dark' ? 'light' : 'dark'),
|
|
73
|
+
fontsLoaded,
|
|
74
|
+
brandFonts,
|
|
75
|
+
}),
|
|
76
|
+
[mode, colors, fontsLoaded, brandFonts],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Theme colors + light/dark mode controls. */
|
|
83
|
+
export function useTheme(): ThemeValue {
|
|
84
|
+
const ctx = useContext(ThemeContext);
|
|
85
|
+
if (!ctx) {
|
|
86
|
+
throw new Error('Myaza KYC components must be rendered inside <MyazaThemeProvider>.');
|
|
87
|
+
}
|
|
88
|
+
return ctx;
|
|
89
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { KYCError } from '../types/verification';
|
|
4
|
+
import type { MyazaKYCConfig, ResolvedKYCConfig } from '../types/config';
|
|
5
|
+
import type { ServerConfigState } from '../store/serverConfig';
|
|
6
|
+
import { requireCountry, resolveWorkflow } from '../services/workflowGate';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Resolving a workflow, separated from anything that draws.
|
|
10
|
+
//
|
|
11
|
+
// This used to live inside the gate, which lived inside the modal — so opening
|
|
12
|
+
// the SDK presented a modal FIRST and resolved afterwards. That ordering is
|
|
13
|
+
// visible: the modal, its chrome and its loader all had to be painted before
|
|
14
|
+
// the workflow's appearance was known, so they came up in the default brand and
|
|
15
|
+
// then changed colour underneath the user. A flash of the wrong brand is worse
|
|
16
|
+
// than a moment of nothing, because it looks like someone else's product.
|
|
17
|
+
//
|
|
18
|
+
// Hoisting resolution out here lets the caller wait for a settled config and
|
|
19
|
+
// present a modal that is correctly branded on its first frame.
|
|
20
|
+
//
|
|
21
|
+
// It also PREFETCHES: resolution starts when the SDK mounts, not when the
|
|
22
|
+
// button is tapped, so by tap time the answer is usually already in hand. That
|
|
23
|
+
// matters — gating the modal without prefetching would just trade a wrong
|
|
24
|
+
// colour for a dead press, which reads as a broken button.
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export interface ResolvedMount {
|
|
28
|
+
config: ResolvedKYCConfig;
|
|
29
|
+
/** Preloaded server config when a workflow supplied one; else undefined. */
|
|
30
|
+
serverConfig?: ServerConfigState;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type MountState =
|
|
34
|
+
| { status: 'resolving' }
|
|
35
|
+
| { status: 'ready'; mount: ResolvedMount }
|
|
36
|
+
| { status: 'error'; error: KYCError };
|
|
37
|
+
|
|
38
|
+
export interface WorkflowMount {
|
|
39
|
+
state: MountState;
|
|
40
|
+
/** Re-run a failed resolution. */
|
|
41
|
+
retry: () => void;
|
|
42
|
+
/**
|
|
43
|
+
* Re-fetch before opening, so a flow edited since the app started is picked
|
|
44
|
+
* up.
|
|
45
|
+
*
|
|
46
|
+
* The prefetch below is a warm-up, NOT a cache: resolving once at mount and
|
|
47
|
+
* reusing it meant an org could publish a change, reopen the SDK, and still
|
|
48
|
+
* get the config fetched when the app launched — with nothing to indicate the
|
|
49
|
+
* screen was stale. A workflow is edited far more often than an app is
|
|
50
|
+
* relaunched, so freshness has to win over saving one request.
|
|
51
|
+
*/
|
|
52
|
+
refresh: () => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Synchronous settle for a props-only mount — no network, no barrier. */
|
|
56
|
+
export function settleWithoutWorkflow(config: MyazaKYCConfig): MountState {
|
|
57
|
+
try {
|
|
58
|
+
return { status: 'ready', mount: { config: requireCountry(config) } };
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return {
|
|
61
|
+
status: 'error',
|
|
62
|
+
error: err instanceof KYCError ? err : new KYCError('unknown', String(err)),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function useWorkflowMount(config: MyazaKYCConfig): WorkflowMount {
|
|
68
|
+
// A props-only mount settles synchronously, so the common case never reports
|
|
69
|
+
// `resolving` for even one frame and the modal opens with no added latency.
|
|
70
|
+
const initial = useRef<MountState | null>(null);
|
|
71
|
+
if (initial.current === null) {
|
|
72
|
+
initial.current = config.workflowId ? { status: 'resolving' } : settleWithoutWorkflow(config);
|
|
73
|
+
}
|
|
74
|
+
const [state, setState] = useState<MountState>(initial.current);
|
|
75
|
+
const [attempt, setAttempt] = useState(0);
|
|
76
|
+
|
|
77
|
+
// Consumers spread props into a fresh object every render, so depending on
|
|
78
|
+
// `config` itself would abort and restart resolution on each of the
|
|
79
|
+
// consumer's re-renders — a loop that never settles. What resolution actually
|
|
80
|
+
// depends on is the key, the host and the workflow id; the rest is read from
|
|
81
|
+
// the ref at call time.
|
|
82
|
+
const configRef = useRef(config);
|
|
83
|
+
configRef.current = config;
|
|
84
|
+
const { workflowId, apiKey, devUrl } = config;
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!workflowId) return;
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
let live = true;
|
|
90
|
+
setState({ status: 'resolving' });
|
|
91
|
+
|
|
92
|
+
resolveWorkflow(configRef.current, controller.signal)
|
|
93
|
+
.then((result) => {
|
|
94
|
+
if (!live) return;
|
|
95
|
+
setState({ status: 'ready', mount: result });
|
|
96
|
+
})
|
|
97
|
+
.catch((err: unknown) => {
|
|
98
|
+
// An abort is this component going away, not a failure to report.
|
|
99
|
+
if (!live || controller.signal.aborted) return;
|
|
100
|
+
setState({
|
|
101
|
+
status: 'error',
|
|
102
|
+
error: err instanceof KYCError ? err : new KYCError('unknown', String(err)),
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return () => {
|
|
107
|
+
live = false;
|
|
108
|
+
controller.abort();
|
|
109
|
+
};
|
|
110
|
+
// `attempt` re-runs this on Retry.
|
|
111
|
+
}, [workflowId, apiKey, devUrl, attempt]);
|
|
112
|
+
|
|
113
|
+
const bump = useCallback(() => setAttempt((n) => n + 1), []);
|
|
114
|
+
|
|
115
|
+
return { state, retry: bump, refresh: bump };
|
|
116
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Svg, { Defs, Mask, Path, Rect } from 'react-native-svg';
|
|
3
|
+
|
|
4
|
+
// The card window within a 3:4 viewBox, centred. Height follows the per-ID
|
|
5
|
+
// guide aspect so the overlay matches the auto-crop (cropCardRegion) — a guide
|
|
6
|
+
// that disagrees with the crop teaches the user to frame it wrong.
|
|
7
|
+
const VB_W = 100;
|
|
8
|
+
const VB_H = 133;
|
|
9
|
+
const GX = 6;
|
|
10
|
+
const GW = 88;
|
|
11
|
+
const C = 7; // corner accent length
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The dimmed surround + dashed document window + primary corner accents.
|
|
15
|
+
*
|
|
16
|
+
* Split out of CameraViewfinder purely for the 200-line rule: it is static
|
|
17
|
+
* geometry with no state, and keeping it here leaves the viewfinder itself
|
|
18
|
+
* about camera wiring rather than path maths.
|
|
19
|
+
*/
|
|
20
|
+
export function CardGuide({
|
|
21
|
+
guideAspect,
|
|
22
|
+
primary,
|
|
23
|
+
}: {
|
|
24
|
+
guideAspect: number;
|
|
25
|
+
primary: string;
|
|
26
|
+
}): React.ReactElement {
|
|
27
|
+
const GH = GW / guideAspect;
|
|
28
|
+
const GY = (VB_H - GH) / 2;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Svg
|
|
32
|
+
style={{ position: 'absolute', inset: 0 }}
|
|
33
|
+
viewBox={`0 0 ${VB_W} ${VB_H}`}
|
|
34
|
+
preserveAspectRatio="none"
|
|
35
|
+
>
|
|
36
|
+
<Defs>
|
|
37
|
+
<Mask id="cardMask">
|
|
38
|
+
<Rect width={VB_W} height={VB_H} fill="white" />
|
|
39
|
+
<Rect x={GX} y={GY} width={GW} height={GH} rx={3} fill="black" />
|
|
40
|
+
</Mask>
|
|
41
|
+
</Defs>
|
|
42
|
+
<Rect width={VB_W} height={VB_H} fill="rgba(0,0,0,0.55)" mask="url(#cardMask)" />
|
|
43
|
+
<Rect
|
|
44
|
+
x={GX}
|
|
45
|
+
y={GY}
|
|
46
|
+
width={GW}
|
|
47
|
+
height={GH}
|
|
48
|
+
rx={3}
|
|
49
|
+
fill="none"
|
|
50
|
+
stroke="rgba(255,255,255,0.85)"
|
|
51
|
+
strokeWidth={0.6}
|
|
52
|
+
strokeDasharray="5 3"
|
|
53
|
+
/>
|
|
54
|
+
<Path d={`M${GX},${GY + C} L${GX},${GY} L${GX + C},${GY}`} stroke={primary} strokeWidth={1.6} fill="none" strokeLinecap="round" />
|
|
55
|
+
<Path d={`M${GX + GW - C},${GY} L${GX + GW},${GY} L${GX + GW},${GY + C}`} stroke={primary} strokeWidth={1.6} fill="none" strokeLinecap="round" />
|
|
56
|
+
<Path d={`M${GX + GW},${GY + GH - C} L${GX + GW},${GY + GH} L${GX + GW - C},${GY + GH}`} stroke={primary} strokeWidth={1.6} fill="none" strokeLinecap="round" />
|
|
57
|
+
<Path d={`M${GX + C},${GY + GH} L${GX},${GY + GH} L${GX},${GY + GH - C}`} stroke={primary} strokeWidth={1.6} fill="none" strokeLinecap="round" />
|
|
58
|
+
</Svg>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { Animated, Easing } from 'react-native';
|
|
3
|
+
import Svg, { Rect } from 'react-native-svg';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Document ghost.
|
|
7
|
+
//
|
|
8
|
+
// A faint outline of the document's LAYOUT inside the capture guide, shown for
|
|
9
|
+
// a few seconds when the camera opens: where the portrait sits, where the
|
|
10
|
+
// printed details run, and — on a passport — the machine-readable band across
|
|
11
|
+
// the bottom.
|
|
12
|
+
//
|
|
13
|
+
// The guide rectangle says WHERE to put the document. It does not say which way
|
|
14
|
+
// round, and the commonest passport mistake is framing the page with the MRZ
|
|
15
|
+
// cropped off the bottom edge — which auto-capture then refuses, because that
|
|
16
|
+
// band is the chip's key and the proof the page is a passport.
|
|
17
|
+
//
|
|
18
|
+
// Placeholder BARS and BLOCKS, never literal drawings: nothing here is meant to
|
|
19
|
+
// be read, only recognised as a shape. A sketched face would be a worse thing
|
|
20
|
+
// to show a user than nothing at all.
|
|
21
|
+
//
|
|
22
|
+
// Ported from the Flutter SDK's DocumentGhost — geometry included, so the two
|
|
23
|
+
// cannot drift.
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** How long the ghost stays before fading. Long enough to register while the
|
|
27
|
+
* user is still lifting the document into frame; short enough to be gone
|
|
28
|
+
* before it can obscure the document itself. */
|
|
29
|
+
const GHOST_MS = 5000;
|
|
30
|
+
|
|
31
|
+
const LINE = 'rgba(255,255,255,0.30)';
|
|
32
|
+
const FILL = 'rgba(255,255,255,0.12)';
|
|
33
|
+
const BAND = 'rgba(255,255,255,0.35)';
|
|
34
|
+
|
|
35
|
+
export function DocumentGhost({
|
|
36
|
+
x,
|
|
37
|
+
y,
|
|
38
|
+
width,
|
|
39
|
+
height,
|
|
40
|
+
showMrzBand,
|
|
41
|
+
documentFound,
|
|
42
|
+
}: {
|
|
43
|
+
/** The guide rect, in the same coordinates the overlay uses. */
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
width: number;
|
|
47
|
+
height: number;
|
|
48
|
+
/** Passport data pages carry a machine-readable band. */
|
|
49
|
+
showMrzBand: boolean;
|
|
50
|
+
/** Hides it early: once a document is framed the guidance has done its job
|
|
51
|
+
* and would only sit on top of the thing being captured. */
|
|
52
|
+
documentFound: boolean;
|
|
53
|
+
}): React.ReactElement | null {
|
|
54
|
+
const opacity = useRef(new Animated.Value(0)).current;
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
Animated.timing(opacity, {
|
|
58
|
+
toValue: 1,
|
|
59
|
+
duration: 400,
|
|
60
|
+
easing: Easing.out(Easing.ease),
|
|
61
|
+
useNativeDriver: true,
|
|
62
|
+
}).start();
|
|
63
|
+
const t = setTimeout(() => {
|
|
64
|
+
Animated.timing(opacity, {
|
|
65
|
+
toValue: 0,
|
|
66
|
+
duration: 600,
|
|
67
|
+
useNativeDriver: true,
|
|
68
|
+
}).start();
|
|
69
|
+
}, GHOST_MS);
|
|
70
|
+
return () => clearTimeout(t);
|
|
71
|
+
}, [opacity]);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!documentFound) return;
|
|
75
|
+
Animated.timing(opacity, { toValue: 0, duration: 250, useNativeDriver: true }).start();
|
|
76
|
+
}, [documentFound, opacity]);
|
|
77
|
+
|
|
78
|
+
if (width <= 0 || height <= 0) return null;
|
|
79
|
+
|
|
80
|
+
// Inset so the layout sits inside the document's edge, not on the guide.
|
|
81
|
+
const pad = width * 0.07;
|
|
82
|
+
const pl = x + pad;
|
|
83
|
+
const pt = y + pad;
|
|
84
|
+
const pw = width - pad * 2;
|
|
85
|
+
const ph = height - pad * 2;
|
|
86
|
+
|
|
87
|
+
const photoW = pw * 0.24;
|
|
88
|
+
const photoH = ph * (showMrzBand ? 0.46 : 0.58);
|
|
89
|
+
|
|
90
|
+
// Alternating lengths so it reads as printed fields rather than a barcode.
|
|
91
|
+
const barLeft = pl + photoW + pw * 0.06;
|
|
92
|
+
const barMax = pl + pw - barLeft;
|
|
93
|
+
const widths = [1.0, 0.62, 0.85, 0.5];
|
|
94
|
+
const barH = Math.min(7, Math.max(3, photoH * 0.1));
|
|
95
|
+
|
|
96
|
+
const bandH = Math.min(8, Math.max(3, ph * 0.055));
|
|
97
|
+
const bandTop = pt + ph - bandH * 3.2;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Animated.View pointerEvents="none" style={{ position: 'absolute', inset: 0, opacity }}>
|
|
101
|
+
<Svg width="100%" height="100%">
|
|
102
|
+
{/* Portrait block — a block, not a face. The proportion identifies it. */}
|
|
103
|
+
<Rect x={pl} y={pt} width={photoW} height={photoH} rx={4} fill={FILL} />
|
|
104
|
+
<Rect
|
|
105
|
+
x={pl}
|
|
106
|
+
y={pt}
|
|
107
|
+
width={photoW}
|
|
108
|
+
height={photoH}
|
|
109
|
+
rx={4}
|
|
110
|
+
fill="none"
|
|
111
|
+
stroke={LINE}
|
|
112
|
+
strokeWidth={1.5}
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
{widths.map((w, i) => {
|
|
116
|
+
const by = pt + photoH * (0.08 + i * 0.26);
|
|
117
|
+
if (by + barH > pt + photoH) return null;
|
|
118
|
+
return (
|
|
119
|
+
<Rect
|
|
120
|
+
key={i}
|
|
121
|
+
x={barLeft}
|
|
122
|
+
y={by}
|
|
123
|
+
width={barMax * w}
|
|
124
|
+
height={barH}
|
|
125
|
+
rx={barH / 2}
|
|
126
|
+
fill={FILL}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
})}
|
|
130
|
+
|
|
131
|
+
{/* The reason the ghost exists: it shows the band belongs INSIDE the
|
|
132
|
+
frame, which is the edge users most often crop away. */}
|
|
133
|
+
{showMrzBand
|
|
134
|
+
? [0, 1].map((i) => (
|
|
135
|
+
<Rect
|
|
136
|
+
key={`b${i}`}
|
|
137
|
+
x={pl}
|
|
138
|
+
y={bandTop + i * bandH * 1.9}
|
|
139
|
+
width={pw * (i === 0 ? 1 : 0.93)}
|
|
140
|
+
height={bandH}
|
|
141
|
+
rx={bandH / 2}
|
|
142
|
+
fill={BAND}
|
|
143
|
+
/>
|
|
144
|
+
))
|
|
145
|
+
: null}
|
|
146
|
+
</Svg>
|
|
147
|
+
</Animated.View>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
const SCRIM = 'rgba(0,0,0,0.55)';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Fixed slots either side of the shutter.
|
|
14
|
+
*
|
|
15
|
+
* The torch is a CAPTURE control, so it sits beside the shutter where the thumb
|
|
16
|
+
* already is — not out in a corner. Equal slots keep the shutter centred
|
|
17
|
+
* whether or not the device has a flash unit to show.
|
|
18
|
+
*/
|
|
19
|
+
const SIDE_SLOT = 72;
|
|
20
|
+
|
|
21
|
+
/** Hint + upload escape + shutter + torch, stacked at the bottom. */
|
|
22
|
+
export function BottomBar({
|
|
23
|
+
hint,
|
|
24
|
+
hintIsAction,
|
|
25
|
+
allowUpload,
|
|
26
|
+
onUpload,
|
|
27
|
+
onCapture,
|
|
28
|
+
busy,
|
|
29
|
+
ready,
|
|
30
|
+
progress,
|
|
31
|
+
hasTorch,
|
|
32
|
+
torch,
|
|
33
|
+
onToggleTorch,
|
|
34
|
+
bottomInset,
|
|
35
|
+
}: {
|
|
36
|
+
hint: string;
|
|
37
|
+
hintIsAction: boolean;
|
|
38
|
+
allowUpload: boolean;
|
|
39
|
+
onUpload: () => void;
|
|
40
|
+
onCapture: () => void;
|
|
41
|
+
busy: boolean;
|
|
42
|
+
ready: boolean;
|
|
43
|
+
progress: number;
|
|
44
|
+
hasTorch: boolean;
|
|
45
|
+
torch: boolean;
|
|
46
|
+
onToggleTorch: () => void;
|
|
47
|
+
bottomInset: number;
|
|
48
|
+
}): React.ReactElement {
|
|
49
|
+
const { colors } = useTheme();
|
|
50
|
+
const R = 40;
|
|
51
|
+
const CIRC = 2 * Math.PI * R;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<View
|
|
55
|
+
style={{
|
|
56
|
+
position: 'absolute',
|
|
57
|
+
left: 0,
|
|
58
|
+
right: 0,
|
|
59
|
+
bottom: bottomInset + spacing.lg,
|
|
60
|
+
alignItems: 'center',
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<View
|
|
64
|
+
style={{
|
|
65
|
+
backgroundColor: hintIsAction ? colors.primary : SCRIM,
|
|
66
|
+
borderRadius: radius.full,
|
|
67
|
+
paddingHorizontal: spacing.md,
|
|
68
|
+
paddingVertical: 10,
|
|
69
|
+
maxWidth: '92%',
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
<MyazaText variant="bodyMedium" color="#FFFFFF" style={{ textAlign: 'center' }}>
|
|
73
|
+
{hint}
|
|
74
|
+
</MyazaText>
|
|
75
|
+
</View>
|
|
76
|
+
|
|
77
|
+
{allowUpload ? (
|
|
78
|
+
<Pressable onPress={onUpload} style={{ marginTop: spacing.md }} accessibilityRole="button">
|
|
79
|
+
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
80
|
+
<Icon name="upload" size={18} color="#FFFFFF" />
|
|
81
|
+
<View style={{ width: 8 }} />
|
|
82
|
+
<MyazaText variant="bodyMedium" color="#FFFFFF">
|
|
83
|
+
Upload a photo instead
|
|
84
|
+
</MyazaText>
|
|
85
|
+
</View>
|
|
86
|
+
</Pressable>
|
|
87
|
+
) : null}
|
|
88
|
+
|
|
89
|
+
<View style={{ height: spacing.md }} />
|
|
90
|
+
|
|
91
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', alignSelf: 'stretch' }}>
|
|
92
|
+
<View style={{ width: SIDE_SLOT }} />
|
|
93
|
+
<View style={{ flex: 1, alignItems: 'center' }}>
|
|
94
|
+
{/* The dwell, drawn around the shutter — a user holding steady can see
|
|
95
|
+
it working instead of wondering whether to press. */}
|
|
96
|
+
{progress > 0 && progress < 1 ? (
|
|
97
|
+
<Svg width={88} height={88} style={{ position: 'absolute', top: -4 }}>
|
|
98
|
+
<Circle
|
|
99
|
+
cx={44}
|
|
100
|
+
cy={44}
|
|
101
|
+
r={R}
|
|
102
|
+
stroke={colors.primary}
|
|
103
|
+
strokeWidth={3}
|
|
104
|
+
fill="none"
|
|
105
|
+
strokeLinecap="round"
|
|
106
|
+
strokeDasharray={CIRC}
|
|
107
|
+
strokeDashoffset={CIRC * (1 - progress)}
|
|
108
|
+
transform="rotate(-90 44 44)"
|
|
109
|
+
/>
|
|
110
|
+
</Svg>
|
|
111
|
+
) : null}
|
|
112
|
+
<Pressable
|
|
113
|
+
onPress={onCapture}
|
|
114
|
+
disabled={busy || !ready}
|
|
115
|
+
accessibilityRole="button"
|
|
116
|
+
accessibilityLabel="Capture photo"
|
|
117
|
+
style={({ pressed }) => ({
|
|
118
|
+
width: 80,
|
|
119
|
+
height: 80,
|
|
120
|
+
borderRadius: radius.full,
|
|
121
|
+
borderWidth: 4,
|
|
122
|
+
borderColor: '#FFFFFF',
|
|
123
|
+
backgroundColor: 'transparent',
|
|
124
|
+
alignItems: 'center',
|
|
125
|
+
justifyContent: 'center',
|
|
126
|
+
opacity: busy ? 0.6 : 1,
|
|
127
|
+
transform: [{ scale: pressed ? 0.94 : 1 }],
|
|
128
|
+
})}
|
|
129
|
+
>
|
|
130
|
+
<View
|
|
131
|
+
style={{
|
|
132
|
+
width: 62,
|
|
133
|
+
height: 62,
|
|
134
|
+
borderRadius: radius.full,
|
|
135
|
+
backgroundColor: ready ? colors.primary : colors.gray400,
|
|
136
|
+
alignItems: 'center',
|
|
137
|
+
justifyContent: 'center',
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
{busy ? <ActivityIndicator color="#FFFFFF" /> : <Icon name="scan-line" size={26} color="#FFFFFF" />}
|
|
141
|
+
</View>
|
|
142
|
+
</Pressable>
|
|
143
|
+
</View>
|
|
144
|
+
<View style={{ width: SIDE_SLOT, alignItems: 'center' }}>
|
|
145
|
+
{hasTorch ? (
|
|
146
|
+
<Pressable
|
|
147
|
+
onPress={onToggleTorch}
|
|
148
|
+
accessibilityRole="button"
|
|
149
|
+
accessibilityLabel={torch ? 'Turn off the torch' : 'Turn on the torch'}
|
|
150
|
+
style={{
|
|
151
|
+
width: 36,
|
|
152
|
+
height: 36,
|
|
153
|
+
borderRadius: radius.full,
|
|
154
|
+
// Inverted when on, so "lit" reads at a glance rather than
|
|
155
|
+
// needing the icon to be decoded.
|
|
156
|
+
backgroundColor: torch ? '#FFFFFF' : 'rgba(0,0,0,0.45)',
|
|
157
|
+
alignItems: 'center',
|
|
158
|
+
justifyContent: 'center',
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
<Icon
|
|
162
|
+
name={torch ? 'zap' : 'zap-off'}
|
|
163
|
+
size={18}
|
|
164
|
+
color={torch ? colors.primary : '#FFFFFF'}
|
|
165
|
+
/>
|
|
166
|
+
</Pressable>
|
|
167
|
+
) : null}
|
|
168
|
+
</View>
|
|
169
|
+
</View>
|
|
170
|
+
</View>
|
|
171
|
+
);
|
|
172
|
+
}
|