@myazahq/kyc-sdk-react-native 2.5.0 → 3.0.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 +277 -7
- package/android/build.gradle +30 -16
- package/android/consumer-rules.pro +22 -0
- package/android/src/main/AndroidManifest.xml +17 -6
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +27 -70
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +14 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/MlKitModelReadiness.kt +154 -0
- package/app.plugin.js +146 -10
- package/ios/HybridMyazaFaceDetector.swift +8 -3
- package/ios/HybridMyazaTextRecognizer.swift +8 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +21 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +8 -0
- package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +16 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +31 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +2 -0
- package/package.json +30 -9
- package/src/MyazaKYC.tsx +4 -11
- package/src/assets/fonts/Karla_400Regular.ttf +0 -0
- package/src/assets/fonts/Karla_500Medium.ttf +0 -0
- package/src/assets/fonts/Karla_600SemiBold.ttf +0 -0
- package/src/assets/fonts/Karla_700Bold.ttf +0 -0
- package/src/assets/fonts/OFL-Karla.txt +93 -0
- package/src/assets/fonts/OFL-SpaceGrotesk.txt +93 -0
- package/src/assets/fonts/README.md +12 -0
- package/src/assets/fonts/SpaceGrotesk_500Medium.ttf +0 -0
- package/src/assets/fonts/SpaceGrotesk_600SemiBold.ttf +0 -0
- package/src/assets/fonts/SpaceGrotesk_700Bold.ttf +0 -0
- package/src/capture/useAutoCapture.ts +8 -1
- package/src/components/CountryField.tsx +8 -0
- package/src/components/CountryRegionPicker.tsx +184 -0
- package/src/components/DialCodePicker.tsx +47 -68
- package/src/components/DialCodeRow.tsx +102 -0
- package/src/components/DocumentCropper.tsx +30 -24
- package/src/components/DocumentReview.tsx +11 -1
- package/src/components/DocumentReviewSide.tsx +6 -2
- package/src/components/DocumentReviewZoom.tsx +5 -1
- package/src/components/FramedMapPicker.tsx +122 -0
- package/src/components/GeoBadge.tsx +34 -0
- package/src/components/Icon.tsx +10 -192
- package/src/components/KycFlow.tsx +69 -34
- package/src/components/LineSkeleton.tsx +100 -0
- package/src/components/MapChrome.tsx +67 -0
- package/src/components/MapPinMarker.tsx +54 -0
- package/src/components/MapPinPicker.tsx +179 -0
- package/src/components/MilestoneTrack.tsx +155 -0
- package/src/components/MyazaButton.tsx +9 -0
- package/src/components/MyazaInput.tsx +17 -1
- package/src/components/OptionRow.tsx +22 -3
- package/src/components/PhoneNumberInput.tsx +8 -0
- package/src/components/PoweredBy.tsx +5 -5
- package/src/components/PresenceBlocks.tsx +153 -0
- package/src/components/RequiredDocumentPill.tsx +114 -0
- package/src/components/StepHeader.tsx +23 -3
- package/src/components/StepView.tsx +17 -0
- package/src/components/StickyActions.tsx +56 -0
- package/src/components/dialCodeRows.ts +64 -0
- package/src/components/documentReviewCopy.ts +39 -0
- package/src/components/fonts.ts +14 -17
- package/src/components/icon-map.ts +176 -0
- package/src/components/stepHeaderMeta.tsx +48 -5
- package/src/config/addressCollection.ts +125 -0
- package/src/config/biometricOptions.ts +100 -0
- package/src/config/business.ts +20 -1
- package/src/config/businessSteps.ts +8 -2
- package/src/config/consentStep.ts +19 -0
- package/src/config/documentCaptureMethods.ts +31 -0
- package/src/config/proofOfAddress.ts +54 -11
- package/src/config/regions.ts +24 -0
- package/src/config/stepOrder.ts +79 -5
- package/src/config/uploadLimits.ts +35 -0
- package/src/config/workflowMerge.ts +13 -5
- package/src/index.ts +49 -1
- package/src/lib/address-current-location.ts +171 -0
- package/src/lib/address-field-modes.ts +119 -0
- package/src/lib/address-flow.ts +172 -0
- package/src/lib/address-helpers.ts +48 -0
- package/src/lib/address-line.ts +76 -0
- package/src/lib/address-pin-move.ts +97 -0
- package/src/lib/address-step-recovery.ts +63 -0
- package/src/lib/authed-image.ts +78 -0
- package/src/lib/biometric-copy.ts +51 -0
- package/src/lib/captureRing.ts +83 -0
- package/src/lib/country-adoption.ts +89 -0
- package/src/lib/documentCaptureCheck.ts +136 -0
- package/src/lib/inferred-country.ts +65 -0
- package/src/lib/livenessLayout.ts +49 -0
- package/src/lib/map-frame.ts +159 -0
- package/src/lib/map-tiles.ts +143 -0
- package/src/lib/model-ready.ts +82 -0
- package/src/lib/poa-country-gate.ts +27 -0
- package/src/lib/prime-models.ts +50 -0
- package/src/lib/resubmit.ts +36 -7
- package/src/lib/result-copy.ts +116 -0
- package/src/lib/result-wait.ts +53 -0
- package/src/lib/review-map-surface.ts +26 -0
- package/src/lib/scope.ts +31 -0
- package/src/lib/selfie-sharpness.ts +211 -0
- package/src/lib/selfie-upload-wait.ts +79 -0
- package/src/lib/street-view-fov.ts +42 -0
- package/src/lib/webview-available.ts +47 -0
- package/src/liveness/avatarSource.ts +59 -0
- package/src/liveness/useLiveness.ts +6 -1
- package/src/liveness/useModelReady.ts +9 -55
- package/src/mrz/extract.ts +71 -4
- package/src/mrz/textRecognizer.ts +36 -0
- package/src/mrz/useTextModelReady.ts +21 -0
- package/src/presence/background-math.ts +100 -0
- package/src/presence/background-store.ts +82 -0
- package/src/presence/background.ts +165 -0
- package/src/presence/foreground-service.ts +193 -0
- package/src/presence/fs.ts +51 -0
- package/src/presence/geofence.ts +41 -0
- package/src/presence/math.ts +44 -0
- package/src/presence/post.ts +40 -0
- package/src/presence/report.ts +87 -0
- package/src/presence/sampler.ts +110 -0
- package/src/presence/status.ts +92 -0
- package/src/presence/store.ts +94 -0
- package/src/presence/tier.ts +36 -0
- package/src/presence/watch-wait.ts +81 -0
- package/src/screens/AddressCountryControl.tsx +115 -0
- package/src/screens/BusinessDetailsFields.tsx +3 -0
- package/src/screens/BusinessDetailsStep.tsx +2 -0
- package/src/screens/BusinessDocumentSlot.tsx +3 -2
- package/src/screens/BusinessDocumentsStep.tsx +4 -55
- package/src/screens/CompanyInfoFields.tsx +6 -1
- package/src/screens/ConsentStep.tsx +6 -17
- package/src/screens/ContactActions.tsx +54 -0
- package/src/screens/ContactDestinationField.tsx +4 -0
- package/src/screens/ContactEntryPanel.tsx +75 -0
- package/src/screens/ContactVerificationStep.tsx +26 -56
- package/src/screens/CountrySelectStep.tsx +13 -147
- package/src/screens/DocumentCaptureStep.tsx +177 -93
- package/src/screens/LivenessAvatar.tsx +33 -16
- package/src/screens/LivenessStep.tsx +188 -49
- package/src/screens/MrzScanView.tsx +33 -0
- package/src/screens/PoaDocumentTypeList.tsx +66 -0
- package/src/screens/ProofOfAddressParts.tsx +151 -0
- package/src/screens/ProofOfAddressStep.tsx +58 -99
- package/src/screens/SubmittedBadge.tsx +25 -0
- package/src/screens/SubmittedError.tsx +64 -0
- package/src/screens/SubmittedResult.tsx +117 -0
- package/src/screens/SubmittedStep.tsx +71 -164
- package/src/screens/SubmittedSuccess.tsx +127 -0
- package/src/screens/SubmittedWaiting.tsx +45 -0
- package/src/screens/address/AddressEntranceStep.tsx +161 -0
- package/src/screens/address/AddressIntroGate.tsx +148 -0
- package/src/screens/address/AddressMapStub.tsx +50 -0
- package/src/screens/address/AddressPinStep.tsx +178 -0
- package/src/screens/address/AddressReviewStep.tsx +162 -0
- package/src/screens/address/AddressSandboxTabs.tsx +158 -0
- package/src/screens/address/AddressSearchField.tsx +115 -0
- package/src/screens/address/AddressSearchStep.tsx +94 -0
- package/src/screens/address/CurrentLocationRow.tsx +135 -0
- package/src/screens/address/DetailsSheet.tsx +175 -0
- package/src/screens/address/DetailsSheetFields.tsx +184 -0
- package/src/screens/address/EntranceDropzone.tsx +178 -0
- package/src/screens/address/EntranceFraming.tsx +66 -0
- package/src/screens/address/EntrancePills.tsx +60 -0
- package/src/screens/address/FramedStreetView.tsx +155 -0
- package/src/screens/address/IntroDisclosures.tsx +210 -0
- package/src/screens/address/LabelDecisionRow.tsx +113 -0
- package/src/screens/address/PinSummaryRow.tsx +105 -0
- package/src/screens/address/ReviewAddressBand.tsx +111 -0
- package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
- package/src/screens/address/ReviewMapPicture.tsx +106 -0
- package/src/screens/address/SearchResults.tsx +105 -0
- package/src/screens/address/SearchScreen.tsx +176 -0
- package/src/screens/address/SkipForNow.tsx +43 -0
- package/src/screens/address/StreetViewChrome.tsx +81 -0
- package/src/screens/address/detail-values.ts +22 -0
- package/src/screens/address/fix-source.ts +27 -0
- package/src/screens/address/index.ts +8 -0
- package/src/screens/address/meta.ts +44 -0
- package/src/screens/address/use-address-flow.ts +200 -0
- package/src/screens/address/use-label-pin.ts +80 -0
- package/src/screens/address/use-pin-actions.ts +192 -0
- package/src/screens/consent/model.ts +79 -10
- package/src/screens/document/CaptureCheckNotice.tsx +77 -0
- package/src/screens/document/UploadPhase.tsx +177 -0
- package/src/screens/document/useDocumentCamera.ts +109 -0
- package/src/screens/liveness/CaptureRing.tsx +91 -0
- package/src/screens/liveness/LivenessCamera.tsx +63 -0
- package/src/screens/liveness/LivenessHandover.tsx +43 -0
- package/src/screens/liveness/LivenessOutcome.tsx +8 -4
- package/src/screens/liveness/SelfiePreview.tsx +31 -7
- package/src/screens/liveness/ShutterFlash.tsx +25 -0
- package/src/screens/liveness/index.ts +4 -0
- package/src/screens/liveness/useSelfieUpload.ts +23 -3
- package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
- package/src/screens/useAddressPhotoAttach.ts +77 -0
- package/src/screens/useBusinessDocumentAttach.ts +93 -0
- package/src/screens/usePoaAttach.ts +12 -15
- package/src/services/api-types.ts +119 -1
- package/src/services/api-verify-types.ts +37 -0
- package/src/services/api.ts +149 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/location.ts +234 -0
- package/src/services/mediaCompress.ts +23 -5
- package/src/services/uploadErrors.ts +30 -0
- package/src/services/workflowGate.ts +7 -0
- package/src/specs/MyazaTextRecognizer.nitro.ts +29 -0
- package/src/store/address.ts +97 -0
- package/src/store/derive.ts +29 -4
- package/src/store/kycStore.ts +120 -5
- package/src/store/serverConfig.ts +18 -0
- package/src/store/session.ts +23 -4
- package/src/store/state.ts +134 -0
- package/src/store/submit.ts +18 -1
- package/src/types/config.ts +63 -1
- package/src/types/verification.ts +12 -0
- package/src/types/workflow.ts +97 -1
- package/src/assets/liveness/Blink.gif +0 -0
- package/src/assets/liveness/Nod.gif +0 -0
- package/src/assets/liveness/Smile.gif +0 -0
- package/src/assets/liveness/Turn.gif +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, StyleSheet, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { spacing } from '../config/theme';
|
|
5
|
+
import { useTheme } from './runtime';
|
|
6
|
+
import { MyazaText } from './Typography';
|
|
7
|
+
import { CountryFlag } from './CountryFlag';
|
|
8
|
+
import { GeoBadge } from './GeoBadge';
|
|
9
|
+
import type { DialCodeOption } from './DialCodePicker';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// One country in the dial-code sheet. Extracted from DialCodePicker (200-line
|
|
13
|
+
// rule) and shared by the pinned geo row and the alphabetical ones, so the two
|
|
14
|
+
// cannot drift apart: they are the same control in two positions, and the
|
|
15
|
+
// pinned one only differs by carrying a tag and a hairline beneath it.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export function DialCodeRow({
|
|
19
|
+
option,
|
|
20
|
+
isSelected,
|
|
21
|
+
badge,
|
|
22
|
+
onPress,
|
|
23
|
+
}: {
|
|
24
|
+
option: DialCodeOption;
|
|
25
|
+
isSelected: boolean;
|
|
26
|
+
/** Tags the pinned geo row ("Your location"). */
|
|
27
|
+
badge?: string;
|
|
28
|
+
onPress: () => void;
|
|
29
|
+
}): React.ReactElement {
|
|
30
|
+
const { colors } = useTheme();
|
|
31
|
+
return (
|
|
32
|
+
<Pressable
|
|
33
|
+
onPress={onPress}
|
|
34
|
+
accessibilityRole="radio"
|
|
35
|
+
accessibilityState={{ checked: isSelected }}
|
|
36
|
+
accessibilityLabel={badge ? `${option.name}, ${badge.toLowerCase()}` : option.name}
|
|
37
|
+
style={{
|
|
38
|
+
flexDirection: 'row',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
paddingHorizontal: spacing.md,
|
|
41
|
+
// Roomier rows: a country list is a long scan, and cramped rows make
|
|
42
|
+
// every tap a precision job. Matches the web dropdown's row height.
|
|
43
|
+
paddingVertical: 12,
|
|
44
|
+
backgroundColor: isSelected ? colors.primary50 : 'transparent',
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
<CountryFlag country={option.code} size={28} />
|
|
48
|
+
<View style={{ width: spacing.md }} />
|
|
49
|
+
{/* No line clamp: "Bosnia & Herzegovina" and the like must read in full,
|
|
50
|
+
which is what Flutter's Expanded(Text) gives. Full body size (16),
|
|
51
|
+
the web dropdown's reading size — 14 read small against the flags. */}
|
|
52
|
+
<MyazaText variant="body" style={{ flex: 1 }}>
|
|
53
|
+
{option.name}
|
|
54
|
+
</MyazaText>
|
|
55
|
+
{badge ? <GeoBadge label={badge} /> : null}
|
|
56
|
+
{option.dialCode != null ? (
|
|
57
|
+
<MyazaText variant="bodyMedium" color={colors.textSecondary}>
|
|
58
|
+
{option.dialCode}
|
|
59
|
+
</MyazaText>
|
|
60
|
+
) : null}
|
|
61
|
+
</Pressable>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A region label between grouped rows — the country-select step's header
|
|
67
|
+
* (uppercase, tracked out), so the two pickers read as one control.
|
|
68
|
+
*/
|
|
69
|
+
export function DialCodeRegionHeader({ region }: { region: string }): React.ReactElement {
|
|
70
|
+
const { colors } = useTheme();
|
|
71
|
+
return (
|
|
72
|
+
<MyazaText
|
|
73
|
+
variant="bodySmall"
|
|
74
|
+
color={colors.textSecondary}
|
|
75
|
+
accessibilityRole="header"
|
|
76
|
+
style={{
|
|
77
|
+
fontWeight: '700',
|
|
78
|
+
letterSpacing: 0.6,
|
|
79
|
+
paddingHorizontal: spacing.md,
|
|
80
|
+
paddingTop: spacing.md,
|
|
81
|
+
paddingBottom: spacing.xs,
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{region.toUpperCase()}
|
|
85
|
+
</MyazaText>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The hairline under the pinned row, separating it from the alphabet. */
|
|
90
|
+
export function DialCodeDivider(): React.ReactElement {
|
|
91
|
+
const { colors } = useTheme();
|
|
92
|
+
return (
|
|
93
|
+
<View
|
|
94
|
+
style={{
|
|
95
|
+
marginHorizontal: spacing.md,
|
|
96
|
+
marginVertical: spacing.xs,
|
|
97
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
98
|
+
borderBottomColor: colors.border,
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -2,11 +2,12 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
2
2
|
import { ActivityIndicator, LayoutChangeEvent, Modal, PanResponder, Platform, Pressable, StatusBar, StyleSheet, View } from 'react-native';
|
|
3
3
|
import Svg, { Line, Rect } from 'react-native-svg';
|
|
4
4
|
|
|
5
|
-
import { spacing } from '../config/theme';
|
|
5
|
+
import { headerSurface, spacing } from '../config/theme';
|
|
6
6
|
import { cropImage, imageSize } from '../services/mediaCompress';
|
|
7
7
|
import { MyazaText } from './Typography';
|
|
8
8
|
import { MyazaButton } from './MyazaButton';
|
|
9
9
|
import { Icon } from './Icon';
|
|
10
|
+
import { useTheme } from './runtime';
|
|
10
11
|
import { Image } from 'react-native';
|
|
11
12
|
|
|
12
13
|
// Interactive ID-card cropper — a 1:1 RN port of the Flutter SDK's
|
|
@@ -19,7 +20,11 @@ import { Image } from 'react-native';
|
|
|
19
20
|
const ID_AR = 85.6 / 53.98;
|
|
20
21
|
const HANDLE_RADIUS = 28; // touch radius for the corner handles
|
|
21
22
|
const ARM = 22; // corner-bracket arm length
|
|
22
|
-
|
|
23
|
+
// The photo sits on a neutral dark stage in both themes (the web SDK's
|
|
24
|
+
// neutral-900): it is the backdrop a document photo reads well against, and it
|
|
25
|
+
// keeps the white crop strokes legible. Everything around the photo takes the
|
|
26
|
+
// workflow's own colours.
|
|
27
|
+
export const CROPPER_STAGE = '#171717';
|
|
23
28
|
// Keep the title clear of the status bar / notch on the full-screen cropper Modal.
|
|
24
29
|
const TOP_INSET = Platform.OS === 'ios' ? 56 : (StatusBar.currentHeight ?? 24) + 8;
|
|
25
30
|
|
|
@@ -95,6 +100,7 @@ export function DocumentCropper({ uri, onCancel, onConfirm }: DocumentCropperPro
|
|
|
95
100
|
const [container, setContainer] = useState<{ w: number; h: number } | null>(null);
|
|
96
101
|
const [crop, setCrop] = useState<Rectangle | null>(null);
|
|
97
102
|
const [processing, setProcessing] = useState(false);
|
|
103
|
+
const { colors, mode } = useTheme();
|
|
98
104
|
|
|
99
105
|
// Pan state held in refs so the PanResponder callbacks stay stable.
|
|
100
106
|
const handleRef = useRef<Handle>('none');
|
|
@@ -219,17 +225,18 @@ export function DocumentCropper({ uri, onCancel, onConfirm }: DocumentCropperPro
|
|
|
219
225
|
|
|
220
226
|
return (
|
|
221
227
|
<Modal visible animationType="slide" presentationStyle="fullScreen" onRequestClose={onCancel}>
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
228
|
+
<StatusBar barStyle={mode === 'dark' ? 'light-content' : 'dark-content'} />
|
|
229
|
+
<View style={{ flex: 1, backgroundColor: CROPPER_STAGE }}>
|
|
230
|
+
{/* App bar: the band the flow's steps sit under */}
|
|
231
|
+
<View style={[styles.appBar, { backgroundColor: headerSurface(colors, mode), borderBottomColor: colors.border }]}>
|
|
232
|
+
<Pressable onPress={onCancel} accessibilityRole="button" accessibilityLabel="Cancel" hitSlop={8} style={styles.close}>
|
|
233
|
+
<Icon name="close" size={20} color={colors.textDark} />
|
|
227
234
|
</Pressable>
|
|
228
|
-
<View>
|
|
229
|
-
<MyazaText variant="bodyMedium" color=
|
|
235
|
+
<View style={{ flexShrink: 1 }}>
|
|
236
|
+
<MyazaText variant="bodyMedium" color={colors.textDark} style={{ fontWeight: '600' }}>
|
|
230
237
|
Crop to ID Card
|
|
231
238
|
</MyazaText>
|
|
232
|
-
<MyazaText variant="bodySmall" color=
|
|
239
|
+
<MyazaText variant="bodySmall" color={colors.textMuted}>
|
|
233
240
|
Drag to reposition · handles to resize
|
|
234
241
|
</MyazaText>
|
|
235
242
|
</View>
|
|
@@ -274,18 +281,9 @@ export function DocumentCropper({ uri, onCancel, onConfirm }: DocumentCropperPro
|
|
|
274
281
|
</View>
|
|
275
282
|
|
|
276
283
|
{/* Action bar */}
|
|
277
|
-
<View style={styles.actionBar}>
|
|
278
|
-
{
|
|
279
|
-
|
|
280
|
-
<ActivityIndicator color="#FFFFFF" />
|
|
281
|
-
<View style={{ width: 10 }} />
|
|
282
|
-
<MyazaText variant="bodyMedium" color="rgba(255,255,255,0.8)" style={{ flexShrink: 1 }}>
|
|
283
|
-
Processing…
|
|
284
|
-
</MyazaText>
|
|
285
|
-
</View>
|
|
286
|
-
) : (
|
|
287
|
-
<MyazaButton label="Crop & Use" leadingIcon="check" onPress={confirm} disabled={!crop} />
|
|
288
|
-
)}
|
|
284
|
+
<View style={[styles.actionBar, { backgroundColor: colors.background, borderTopColor: colors.border }]}>
|
|
285
|
+
{/* The flow's own button: primary from the workflow, its loading state while the crop runs. */}
|
|
286
|
+
<MyazaButton label="Crop & Use" leadingIcon="check" onPress={confirm} disabled={!crop} loading={processing} />
|
|
289
287
|
</View>
|
|
290
288
|
</View>
|
|
291
289
|
</Modal>
|
|
@@ -311,15 +309,23 @@ const styles = StyleSheet.create({
|
|
|
311
309
|
appBar: {
|
|
312
310
|
flexDirection: 'row',
|
|
313
311
|
alignItems: 'center',
|
|
314
|
-
backgroundColor: APPBAR_BG,
|
|
315
312
|
paddingTop: TOP_INSET,
|
|
316
313
|
paddingBottom: spacing.sm,
|
|
317
314
|
paddingHorizontal: spacing.sm,
|
|
315
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
316
|
+
},
|
|
317
|
+
// 44pt: the tap target the platforms ask for, around a 20pt glyph.
|
|
318
|
+
close: {
|
|
319
|
+
width: 44,
|
|
320
|
+
height: 44,
|
|
321
|
+
alignItems: 'center',
|
|
322
|
+
justifyContent: 'center',
|
|
323
|
+
marginRight: 4,
|
|
318
324
|
},
|
|
319
325
|
actionBar: {
|
|
320
|
-
backgroundColor: APPBAR_BG,
|
|
321
326
|
paddingHorizontal: spacing.md,
|
|
322
327
|
paddingTop: spacing.sm,
|
|
323
328
|
paddingBottom: spacing.lg,
|
|
329
|
+
borderTopWidth: StyleSheet.hairlineWidth,
|
|
324
330
|
},
|
|
325
331
|
});
|
|
@@ -6,6 +6,7 @@ import { Icon } from './Icon';
|
|
|
6
6
|
import { MyazaText } from './Typography';
|
|
7
7
|
import { DocumentReviewThumb, type ReviewSide } from './DocumentReviewSide';
|
|
8
8
|
import { DocumentReviewZoom } from './DocumentReviewZoom';
|
|
9
|
+
import { documentReviewCopy, type DocumentCaptureMode } from './documentReviewCopy';
|
|
9
10
|
import { radius, spacing } from '../config/theme';
|
|
10
11
|
|
|
11
12
|
// ─── Document review ──────────────────────────────────────────────────────────
|
|
@@ -33,6 +34,9 @@ import { radius, spacing } from '../config/theme';
|
|
|
33
34
|
// Unlike Flutter, the step here sits inside the sheet's own ScrollView (which
|
|
34
35
|
// already carries the bottom safe-area inset), so the footer is pushed down by
|
|
35
36
|
// a flexible spacer rather than pinned with a hand-computed inset.
|
|
37
|
+
//
|
|
38
|
+
// The words follow `mode` (see documentReviewCopy): an upload-only workflow
|
|
39
|
+
// never showed a camera, so it says "Replace" and "added".
|
|
36
40
|
|
|
37
41
|
export function DocumentReview({
|
|
38
42
|
frontUri,
|
|
@@ -43,6 +47,7 @@ export function DocumentReview({
|
|
|
43
47
|
footer,
|
|
44
48
|
onRetakeFront,
|
|
45
49
|
onRetakeBack,
|
|
50
|
+
mode = 'scan',
|
|
46
51
|
}: {
|
|
47
52
|
frontUri: string;
|
|
48
53
|
backUri?: string | null;
|
|
@@ -56,6 +61,8 @@ export function DocumentReview({
|
|
|
56
61
|
footer: React.ReactNode;
|
|
57
62
|
onRetakeFront?: () => void;
|
|
58
63
|
onRetakeBack?: () => void;
|
|
64
|
+
/** How the photos got in: the camera (default) or chosen from the device. */
|
|
65
|
+
mode?: DocumentCaptureMode;
|
|
59
66
|
}): React.ReactElement {
|
|
60
67
|
const { colors } = useTheme();
|
|
61
68
|
const [zoomed, setZoomed] = useState<ReviewSide | null>(null);
|
|
@@ -65,6 +72,7 @@ export function DocumentReview({
|
|
|
65
72
|
...(backUri ? [{ id: 'back' as const, label: 'Back', uri: backUri, onRetake: onRetakeBack }] : []),
|
|
66
73
|
];
|
|
67
74
|
const twoSided = sides.length > 1;
|
|
75
|
+
const copy = documentReviewCopy(mode, twoSided);
|
|
68
76
|
|
|
69
77
|
return (
|
|
70
78
|
<View style={{ flex: 1 }}>
|
|
@@ -85,7 +93,7 @@ export function DocumentReview({
|
|
|
85
93
|
</View>
|
|
86
94
|
<View style={{ width: spacing.xs }} />
|
|
87
95
|
<MyazaText variant="bodySmall" style={{ fontWeight: '600' }}>
|
|
88
|
-
{
|
|
96
|
+
{copy.status}
|
|
89
97
|
</MyazaText>
|
|
90
98
|
</View>
|
|
91
99
|
|
|
@@ -108,6 +116,7 @@ export function DocumentReview({
|
|
|
108
116
|
// the screen read as permanently stuck.
|
|
109
117
|
busyOverlay={isBusy ? busyOverlay : null}
|
|
110
118
|
onZoom={() => setZoomed(side)}
|
|
119
|
+
copy={copy}
|
|
111
120
|
/>
|
|
112
121
|
</View>
|
|
113
122
|
</React.Fragment>
|
|
@@ -129,6 +138,7 @@ export function DocumentReview({
|
|
|
129
138
|
<DocumentReviewZoom
|
|
130
139
|
side={zoomed}
|
|
131
140
|
isBusy={isBusy}
|
|
141
|
+
copy={copy}
|
|
132
142
|
onClose={() => setZoomed(null)}
|
|
133
143
|
onRetake={() => {
|
|
134
144
|
const retake = zoomed.onRetake;
|
|
@@ -6,6 +6,7 @@ import { Icon } from './Icon';
|
|
|
6
6
|
import { MyazaText } from './Typography';
|
|
7
7
|
import { radius, spacing } from '../config/theme';
|
|
8
8
|
import { CHROME_SCRIM, ChromeGlass } from './glass/ChromeGlass';
|
|
9
|
+
import type { DocumentReviewCopy } from './documentReviewCopy';
|
|
9
10
|
|
|
10
11
|
/** The chip's flat backing, and still the fallback off iOS 26. */
|
|
11
12
|
const CHIP_SCRIM = CHROME_SCRIM;
|
|
@@ -34,12 +35,15 @@ export function DocumentReviewThumb({
|
|
|
34
35
|
isBusy,
|
|
35
36
|
busyOverlay,
|
|
36
37
|
onZoom,
|
|
38
|
+
copy,
|
|
37
39
|
}: {
|
|
38
40
|
side: ReviewSide;
|
|
39
41
|
aspect: number;
|
|
40
42
|
isBusy: boolean;
|
|
41
43
|
busyOverlay?: React.ReactNode;
|
|
42
44
|
onZoom: () => void;
|
|
45
|
+
/** The review's words for the capture mode (documentReviewCopy). */
|
|
46
|
+
copy: DocumentReviewCopy;
|
|
43
47
|
}): React.ReactElement {
|
|
44
48
|
const { colors } = useTheme();
|
|
45
49
|
return (
|
|
@@ -90,7 +94,7 @@ export function DocumentReviewThumb({
|
|
|
90
94
|
onPress={isBusy ? undefined : side.onRetake}
|
|
91
95
|
disabled={isBusy}
|
|
92
96
|
accessibilityRole="button"
|
|
93
|
-
accessibilityLabel={
|
|
97
|
+
accessibilityLabel={copy.redoAccessibility(side.label)}
|
|
94
98
|
style={{
|
|
95
99
|
flexDirection: 'row',
|
|
96
100
|
alignItems: 'center',
|
|
@@ -103,7 +107,7 @@ export function DocumentReviewThumb({
|
|
|
103
107
|
<Icon name="refresh" size={14} color={colors.primary} />
|
|
104
108
|
<View style={{ width: 6 }} />
|
|
105
109
|
<MyazaText variant="bodySmall" color={colors.primary} style={{ flexShrink: 1 }}>
|
|
106
|
-
|
|
110
|
+
{copy.redo}
|
|
107
111
|
</MyazaText>
|
|
108
112
|
</Pressable>
|
|
109
113
|
</View>
|
|
@@ -7,6 +7,7 @@ import { Icon } from './Icon';
|
|
|
7
7
|
import { MyazaText } from './Typography';
|
|
8
8
|
import { MyazaButton } from './MyazaButton';
|
|
9
9
|
import { Chip, CHIP_TEXT, type ReviewSide } from './DocumentReviewSide';
|
|
10
|
+
import type { DocumentReviewCopy } from './documentReviewCopy';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Full-bleed view of one side, with retake to hand — checking a capture and
|
|
@@ -43,11 +44,14 @@ function bottomInset(): number {
|
|
|
43
44
|
export function DocumentReviewZoom({
|
|
44
45
|
side,
|
|
45
46
|
isBusy,
|
|
47
|
+
copy,
|
|
46
48
|
onClose,
|
|
47
49
|
onRetake,
|
|
48
50
|
}: {
|
|
49
51
|
side: ReviewSide;
|
|
50
52
|
isBusy: boolean;
|
|
53
|
+
/** The review's words for the capture mode (documentReviewCopy). */
|
|
54
|
+
copy: DocumentReviewCopy;
|
|
51
55
|
onClose: () => void;
|
|
52
56
|
onRetake: () => void;
|
|
53
57
|
}): React.ReactElement {
|
|
@@ -107,7 +111,7 @@ export function DocumentReviewZoom({
|
|
|
107
111
|
|
|
108
112
|
<View style={{ height: spacing.sm }} />
|
|
109
113
|
<MyazaButton
|
|
110
|
-
label={
|
|
114
|
+
label={copy.redoSide(side.label)}
|
|
111
115
|
leadingIcon="refresh"
|
|
112
116
|
variant="outline"
|
|
113
117
|
disabled={isBusy}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { ActivityIndicator, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius } from '../config/theme';
|
|
5
|
+
import { loadWebView, type WebViewLike } from '../lib/webview-available';
|
|
6
|
+
import { useTheme } from './runtime';
|
|
7
|
+
import { MapPinPicker } from './MapPinPicker';
|
|
8
|
+
import {
|
|
9
|
+
MAP_FRAME_READY_TIMEOUT_MS,
|
|
10
|
+
buildMapFrameSrc,
|
|
11
|
+
centerCommandScript,
|
|
12
|
+
parseMapFrameMessage,
|
|
13
|
+
samePin,
|
|
14
|
+
} from '../lib/map-frame';
|
|
15
|
+
import type { LatLng } from '../lib/map-tiles';
|
|
16
|
+
|
|
17
|
+
// Google Maps on mobile, via OUR hosted /embed/map page in a WebView (the OkHi
|
|
18
|
+
// model — the map runs on the hosted origin, on Myaza's own key; the host app
|
|
19
|
+
// never brings one). Mirrors the web SDK's FramedMapPicker: a page that never
|
|
20
|
+
// says `ready` (no webview module installed, blocked script, refused grant,
|
|
21
|
+
// vendor outage) falls back to the dependency-free OSM picker, so the step can
|
|
22
|
+
// never go blank. `react-native-webview` is an OPTIONAL peer: absent, this
|
|
23
|
+
// component IS the OSM picker.
|
|
24
|
+
|
|
25
|
+
interface FramedMapPickerProps {
|
|
26
|
+
/** The server-minted page URL; null ⇒ the OSM picker outright. */
|
|
27
|
+
frameUrl: string | null;
|
|
28
|
+
value: LatLng | null;
|
|
29
|
+
/** Corner rounding. 0 where a parent already shapes the frame — the review
|
|
30
|
+
* card rounds its TOP corners only, and the picker's own radius put a curve
|
|
31
|
+
* where the map meets the address band. */
|
|
32
|
+
cornerRadius?: number;
|
|
33
|
+
onChange: (pin: LatLng) => void;
|
|
34
|
+
defaultCenter: LatLng;
|
|
35
|
+
defaultZoom: number;
|
|
36
|
+
height?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function FramedMapPicker({ frameUrl, value, onChange, defaultCenter, defaultZoom, height = 300, cornerRadius }: FramedMapPickerProps): React.ReactElement {
|
|
40
|
+
const WebView = useMemo(loadWebView, []);
|
|
41
|
+
const { colors, mode } = useTheme();
|
|
42
|
+
const [ready, setReady] = useState(false);
|
|
43
|
+
const [failed, setFailed] = useState(!frameUrl || !WebView);
|
|
44
|
+
const readyRef = useRef(false);
|
|
45
|
+
const webRef = useRef<WebViewLike>(null);
|
|
46
|
+
const onChangeRef = useRef(onChange);
|
|
47
|
+
onChangeRef.current = onChange;
|
|
48
|
+
const lastFromFrame = useRef<LatLng | null>(null);
|
|
49
|
+
|
|
50
|
+
// Built once per mount: a src that tracked the pin would reload the page per drag.
|
|
51
|
+
const src = useMemo(
|
|
52
|
+
() =>
|
|
53
|
+
frameUrl
|
|
54
|
+
? buildMapFrameSrc(frameUrl, {
|
|
55
|
+
center: value ?? defaultCenter,
|
|
56
|
+
zoom: defaultZoom,
|
|
57
|
+
hasPin: value != null,
|
|
58
|
+
theme: mode === 'dark' ? 'dark' : 'light',
|
|
59
|
+
primaryColor: colors.primary,
|
|
60
|
+
})
|
|
61
|
+
: null,
|
|
62
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
63
|
+
[frameUrl],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (failed) return undefined;
|
|
68
|
+
const timeout = setTimeout(() => {
|
|
69
|
+
if (!readyRef.current) setFailed(true);
|
|
70
|
+
}, MAP_FRAME_READY_TIMEOUT_MS);
|
|
71
|
+
return () => clearTimeout(timeout);
|
|
72
|
+
}, [failed]);
|
|
73
|
+
|
|
74
|
+
// An external recentre (Use my location, restored progress) moves the map —
|
|
75
|
+
// but never echo back a pin the page itself just reported.
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!value || !ready || samePin(lastFromFrame.current, value)) return;
|
|
78
|
+
webRef.current?.injectJavaScript(centerCommandScript(value));
|
|
79
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
80
|
+
}, [value?.lat, value?.lng, ready]);
|
|
81
|
+
|
|
82
|
+
if (failed || !WebView || !src) {
|
|
83
|
+
return <MapPinPicker value={value} onChange={onChange} defaultCenter={defaultCenter} defaultZoom={defaultZoom} height={height} cornerRadius={cornerRadius} />;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<View style={{ height, borderRadius: cornerRadius ?? radius.md, overflow: 'hidden', backgroundColor: colors.backgroundSecondary }}>
|
|
88
|
+
<WebView
|
|
89
|
+
ref={webRef}
|
|
90
|
+
source={{ uri: src }}
|
|
91
|
+
style={{ flex: 1, backgroundColor: 'transparent' }}
|
|
92
|
+
javaScriptEnabled
|
|
93
|
+
// Android: this WebView sits inside StickyActions' ScrollView, and a
|
|
94
|
+
// ScrollView intercepts every vertical drag its children start, so the
|
|
95
|
+
// map moved a few pixels and stopped while the page scrolled instead.
|
|
96
|
+
// nestedScrollEnabled makes the WebView ask the parent not to intercept
|
|
97
|
+
// (requestDisallowInterceptTouchEvent) for the life of the touch. No-op
|
|
98
|
+
// on iOS. Reported on a Galaxy S24, 2026-09-07.
|
|
99
|
+
nestedScrollEnabled
|
|
100
|
+
onError={() => setFailed(true)}
|
|
101
|
+
onMessage={(event) => {
|
|
102
|
+
const msg = parseMapFrameMessage(event.nativeEvent.data);
|
|
103
|
+
if (!msg) return;
|
|
104
|
+
if (msg.type === 'ready') {
|
|
105
|
+
readyRef.current = true;
|
|
106
|
+
setReady(true);
|
|
107
|
+
} else if (msg.type === 'failed') {
|
|
108
|
+
setFailed(true);
|
|
109
|
+
} else {
|
|
110
|
+
lastFromFrame.current = { lat: msg.lat, lng: msg.lng };
|
|
111
|
+
onChangeRef.current({ lat: msg.lat, lng: msg.lng });
|
|
112
|
+
}
|
|
113
|
+
}}
|
|
114
|
+
/>
|
|
115
|
+
{!ready ? (
|
|
116
|
+
<View pointerEvents="none" style={{ position: 'absolute', top: 0, right: 0, bottom: 0, left: 0, alignItems: 'center', justifyContent: 'center' }}>
|
|
117
|
+
<ActivityIndicator size="small" color={colors.primary} />
|
|
118
|
+
</View>
|
|
119
|
+
) : null}
|
|
120
|
+
</View>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The "Your location" pill beside a pinned geo row: primary tint, primary
|
|
10
|
+
* text. It marks a GUESS made on the visitor's behalf, never a verdict, and
|
|
11
|
+
* the row it sits on is one tap away rather than buried in the alphabet.
|
|
12
|
+
* Shared by the country-select picker and the dial-code sheet so the tag
|
|
13
|
+
* reads identically wherever the guess is offered.
|
|
14
|
+
*/
|
|
15
|
+
export function GeoBadge({ label }: { label: string }): React.ReactElement {
|
|
16
|
+
const { colors } = useTheme();
|
|
17
|
+
return (
|
|
18
|
+
<View
|
|
19
|
+
style={{
|
|
20
|
+
marginRight: spacing.sm,
|
|
21
|
+
paddingHorizontal: spacing.sm,
|
|
22
|
+
paddingVertical: 2,
|
|
23
|
+
borderRadius: radius.full,
|
|
24
|
+
borderWidth: 1,
|
|
25
|
+
borderColor: colors.primary,
|
|
26
|
+
backgroundColor: colors.primary50,
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
<MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '500' }}>
|
|
30
|
+
{label}
|
|
31
|
+
</MyazaText>
|
|
32
|
+
</View>
|
|
33
|
+
);
|
|
34
|
+
}
|