@myazahq/kyc-sdk-react-native 2.5.0 → 2.6.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.
Files changed (167) hide show
  1. package/README.md +151 -5
  2. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +6 -4
  3. package/app.plugin.js +89 -8
  4. package/ios/HybridMyazaFaceDetector.swift +8 -3
  5. package/package.json +14 -2
  6. package/src/MyazaBiometricAuth.tsx +119 -0
  7. package/src/components/CountryField.tsx +8 -0
  8. package/src/components/CountryRegionPicker.tsx +184 -0
  9. package/src/components/DialCodePicker.tsx +47 -68
  10. package/src/components/DialCodeRow.tsx +102 -0
  11. package/src/components/FramedMapPicker.tsx +122 -0
  12. package/src/components/GeoBadge.tsx +34 -0
  13. package/src/components/Icon.tsx +2 -185
  14. package/src/components/KycFlow.tsx +60 -32
  15. package/src/components/LineSkeleton.tsx +100 -0
  16. package/src/components/MapChrome.tsx +67 -0
  17. package/src/components/MapPinMarker.tsx +54 -0
  18. package/src/components/MapPinPicker.tsx +179 -0
  19. package/src/components/MilestoneTrack.tsx +155 -0
  20. package/src/components/MyazaInput.tsx +17 -1
  21. package/src/components/OptionRow.tsx +22 -3
  22. package/src/components/PhoneNumberInput.tsx +8 -0
  23. package/src/components/PoweredBy.tsx +5 -5
  24. package/src/components/PresenceBlocks.tsx +153 -0
  25. package/src/components/StepView.tsx +17 -0
  26. package/src/components/StickyActions.tsx +56 -0
  27. package/src/components/dialCodeRows.ts +64 -0
  28. package/src/components/icon-map.ts +176 -0
  29. package/src/components/stepHeaderMeta.tsx +42 -4
  30. package/src/config/addressCollection.ts +125 -0
  31. package/src/config/biometricOptions.ts +100 -0
  32. package/src/config/business.ts +20 -1
  33. package/src/config/businessSteps.ts +8 -2
  34. package/src/config/consentStep.ts +19 -0
  35. package/src/config/proofOfAddress.ts +54 -11
  36. package/src/config/regions.ts +24 -0
  37. package/src/config/stepOrder.ts +79 -5
  38. package/src/config/uploadLimits.ts +35 -0
  39. package/src/config/workflowMerge.ts +11 -5
  40. package/src/index.ts +54 -1
  41. package/src/lib/address-current-location.ts +171 -0
  42. package/src/lib/address-field-modes.ts +119 -0
  43. package/src/lib/address-flow.ts +172 -0
  44. package/src/lib/address-helpers.ts +48 -0
  45. package/src/lib/address-line.ts +76 -0
  46. package/src/lib/address-pin-move.ts +97 -0
  47. package/src/lib/address-step-recovery.ts +63 -0
  48. package/src/lib/authed-image.ts +78 -0
  49. package/src/lib/biometric-auth.ts +52 -0
  50. package/src/lib/biometric-copy.ts +51 -0
  51. package/src/lib/captureRing.ts +83 -0
  52. package/src/lib/country-adoption.ts +89 -0
  53. package/src/lib/inferred-country.ts +65 -0
  54. package/src/lib/livenessLayout.ts +49 -0
  55. package/src/lib/map-frame.ts +159 -0
  56. package/src/lib/map-tiles.ts +143 -0
  57. package/src/lib/poa-country-gate.ts +27 -0
  58. package/src/lib/result-copy.ts +116 -0
  59. package/src/lib/result-wait.ts +53 -0
  60. package/src/lib/review-map-surface.ts +26 -0
  61. package/src/lib/scope.ts +31 -0
  62. package/src/lib/selfie-upload-wait.ts +79 -0
  63. package/src/lib/street-view-fov.ts +42 -0
  64. package/src/lib/webview-available.ts +47 -0
  65. package/src/liveness/useLiveness.ts +6 -1
  66. package/src/presence/background-math.ts +100 -0
  67. package/src/presence/background-store.ts +82 -0
  68. package/src/presence/background.ts +165 -0
  69. package/src/presence/foreground-service.ts +193 -0
  70. package/src/presence/fs.ts +51 -0
  71. package/src/presence/geofence.ts +41 -0
  72. package/src/presence/math.ts +44 -0
  73. package/src/presence/post.ts +40 -0
  74. package/src/presence/report.ts +87 -0
  75. package/src/presence/sampler.ts +110 -0
  76. package/src/presence/status.ts +92 -0
  77. package/src/presence/store.ts +94 -0
  78. package/src/presence/tier.ts +36 -0
  79. package/src/presence/watch-wait.ts +81 -0
  80. package/src/screens/AddressCountryControl.tsx +115 -0
  81. package/src/screens/BusinessDetailsFields.tsx +3 -0
  82. package/src/screens/BusinessDetailsStep.tsx +2 -0
  83. package/src/screens/BusinessDocumentSlot.tsx +3 -2
  84. package/src/screens/BusinessDocumentsStep.tsx +4 -55
  85. package/src/screens/CompanyInfoFields.tsx +6 -1
  86. package/src/screens/ConsentStep.tsx +6 -17
  87. package/src/screens/ContactActions.tsx +54 -0
  88. package/src/screens/ContactDestinationField.tsx +4 -0
  89. package/src/screens/ContactEntryPanel.tsx +75 -0
  90. package/src/screens/ContactVerificationStep.tsx +26 -56
  91. package/src/screens/CountrySelectStep.tsx +13 -147
  92. package/src/screens/LivenessAvatar.tsx +15 -3
  93. package/src/screens/LivenessStep.tsx +154 -52
  94. package/src/screens/PoaDocumentTypeList.tsx +66 -0
  95. package/src/screens/ProofOfAddressParts.tsx +151 -0
  96. package/src/screens/ProofOfAddressStep.tsx +58 -99
  97. package/src/screens/SubmittedBadge.tsx +25 -0
  98. package/src/screens/SubmittedError.tsx +64 -0
  99. package/src/screens/SubmittedResult.tsx +117 -0
  100. package/src/screens/SubmittedStep.tsx +71 -164
  101. package/src/screens/SubmittedSuccess.tsx +127 -0
  102. package/src/screens/SubmittedWaiting.tsx +45 -0
  103. package/src/screens/address/AddressEntranceStep.tsx +161 -0
  104. package/src/screens/address/AddressIntroGate.tsx +148 -0
  105. package/src/screens/address/AddressMapStub.tsx +50 -0
  106. package/src/screens/address/AddressPinStep.tsx +178 -0
  107. package/src/screens/address/AddressReviewStep.tsx +162 -0
  108. package/src/screens/address/AddressSandboxTabs.tsx +158 -0
  109. package/src/screens/address/AddressSearchField.tsx +115 -0
  110. package/src/screens/address/AddressSearchStep.tsx +94 -0
  111. package/src/screens/address/CurrentLocationRow.tsx +135 -0
  112. package/src/screens/address/DetailsSheet.tsx +175 -0
  113. package/src/screens/address/DetailsSheetFields.tsx +184 -0
  114. package/src/screens/address/EntranceDropzone.tsx +178 -0
  115. package/src/screens/address/EntranceFraming.tsx +66 -0
  116. package/src/screens/address/EntrancePills.tsx +60 -0
  117. package/src/screens/address/FramedStreetView.tsx +155 -0
  118. package/src/screens/address/IntroDisclosures.tsx +210 -0
  119. package/src/screens/address/LabelDecisionRow.tsx +113 -0
  120. package/src/screens/address/PinSummaryRow.tsx +105 -0
  121. package/src/screens/address/ReviewAddressBand.tsx +111 -0
  122. package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
  123. package/src/screens/address/ReviewMapPicture.tsx +106 -0
  124. package/src/screens/address/SearchResults.tsx +105 -0
  125. package/src/screens/address/SearchScreen.tsx +176 -0
  126. package/src/screens/address/SkipForNow.tsx +43 -0
  127. package/src/screens/address/StreetViewChrome.tsx +81 -0
  128. package/src/screens/address/detail-values.ts +22 -0
  129. package/src/screens/address/fix-source.ts +27 -0
  130. package/src/screens/address/index.ts +8 -0
  131. package/src/screens/address/meta.ts +44 -0
  132. package/src/screens/address/use-address-flow.ts +200 -0
  133. package/src/screens/address/use-label-pin.ts +80 -0
  134. package/src/screens/address/use-pin-actions.ts +192 -0
  135. package/src/screens/biometric/BiometricAuthFlow.tsx +170 -0
  136. package/src/screens/consent/model.ts +79 -10
  137. package/src/screens/liveness/CaptureRing.tsx +91 -0
  138. package/src/screens/liveness/LivenessCamera.tsx +63 -0
  139. package/src/screens/liveness/LivenessHandover.tsx +43 -0
  140. package/src/screens/liveness/LivenessOutcome.tsx +4 -3
  141. package/src/screens/liveness/SelfiePreview.tsx +13 -4
  142. package/src/screens/liveness/ShutterFlash.tsx +25 -0
  143. package/src/screens/liveness/index.ts +4 -0
  144. package/src/screens/liveness/useSelfieUpload.ts +23 -3
  145. package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
  146. package/src/screens/useAddressPhotoAttach.ts +77 -0
  147. package/src/screens/useBusinessDocumentAttach.ts +93 -0
  148. package/src/screens/usePoaAttach.ts +12 -15
  149. package/src/services/api-biometric.ts +37 -0
  150. package/src/services/api-types-biometric.ts +41 -0
  151. package/src/services/api-types.ts +93 -1
  152. package/src/services/api-verify-types.ts +37 -0
  153. package/src/services/api.ts +133 -3
  154. package/src/services/deviceMetadata.ts +1 -1
  155. package/src/services/location.ts +234 -0
  156. package/src/services/uploadErrors.ts +30 -0
  157. package/src/services/workflowGate.ts +7 -0
  158. package/src/store/address.ts +97 -0
  159. package/src/store/derive.ts +29 -4
  160. package/src/store/kycStore.ts +112 -3
  161. package/src/store/serverConfig.ts +18 -0
  162. package/src/store/session.ts +23 -4
  163. package/src/store/state.ts +134 -0
  164. package/src/store/submit.ts +18 -1
  165. package/src/types/config.ts +48 -1
  166. package/src/types/verification.ts +12 -0
  167. package/src/types/workflow.ts +97 -1
@@ -0,0 +1,148 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { radius, spacing } from '../../config/theme';
5
+ import { useKyc, useKycConfig, useKycStore, useTheme } from '../../components/runtime';
6
+ import { MyazaText } from '../../components/Typography';
7
+ import { MyazaButton } from '../../components/MyazaButton';
8
+ import { Icon } from '../../components/Icon';
9
+ import { MilestoneTrack, type Milestone } from '../../components/MilestoneTrack';
10
+ import { IntroDisclosures } from './IntroDisclosures';
11
+ import type { KYCStep } from '../../types/config';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // The presence "how it works" screen, shown ONCE before the address flow's
15
+ // first step. Drawn in the SUCCESS CARD's language — eyebrow pill, header band,
16
+ // milestone track — so the promise made here and the "address check active"
17
+ // card at the end read as two frames of one story.
18
+ //
19
+ // The underlying step still does its mount work behind this gate: the
20
+ // current-fix prefetch starts while the primer is on screen, so by the time
21
+ // "Got it" is tapped the fix is usually already in hand and the pin lands with
22
+ // no hesitation. Only the APPLY waits.
23
+ // ---------------------------------------------------------------------------
24
+
25
+ // The second milestone's caption depends on which tier the org runs. With
26
+ // background geofencing on, the "allow all the time" prompt is coming, and
27
+ // OkHi's integration guidance is to say so ONCE, up front, beside the
28
+ // education — not to surprise the person with it after capture.
29
+ const CHECK_IN_CAPTION = {
30
+ foreground: 'Keep location on; your phone confirms it over the coming days.',
31
+ background:
32
+ 'Allow location all the time when asked. Your phone then confirms it on its own, even with the app closed.',
33
+ } as const;
34
+
35
+ const milestonesFor = (background: boolean): readonly Milestone[] => [
36
+ {
37
+ icon: 'map-pin-house',
38
+ stage: 'Your part',
39
+ title: 'Pin your address',
40
+ caption: 'Put the pin right on your building. Takes a minute.',
41
+ state: 'active',
42
+ },
43
+ {
44
+ icon: 'radar',
45
+ stage: 'After that',
46
+ title: 'Quiet check-ins',
47
+ caption: background ? CHECK_IN_CAPTION.background : CHECK_IN_CAPTION.foreground,
48
+ state: 'ahead',
49
+ },
50
+ {
51
+ icon: 'bell-ring',
52
+ stage: 'Then',
53
+ title: 'Confirmed',
54
+ caption: 'You’ll be notified. That is it.',
55
+ state: 'ahead',
56
+ },
57
+ ];
58
+
59
+ /**
60
+ * The gate for `step`, or null when it should not show. Returning the element
61
+ * (rather than a boolean) keeps the decision and the screen in one place: a
62
+ * step renders `gate ?? its own body`.
63
+ */
64
+ export function useAddressIntroGate(
65
+ step: KYCStep,
66
+ firstStep: KYCStep,
67
+ ): React.ReactElement | null {
68
+ const config = useKycConfig();
69
+ const store = useKycStore();
70
+ const { colors } = useTheme();
71
+ const seen = useKyc((s) => s.addressIntroSeen);
72
+ const presence = config.addressCollection?.presence?.enabled === true;
73
+ const background = config.addressCollection?.presence?.background === true;
74
+
75
+ if (step !== firstStep || !presence || seen) return null;
76
+
77
+ return (
78
+ <View>
79
+ {/* Web's exact tints (border-primary/15, bg-primary/[0.06], the badge
80
+ on primary/10), as hex alpha on the primary so a custom brand and
81
+ dark mode both keep the relationship; Flutter draws the same. */}
82
+ <View
83
+ style={{
84
+ borderWidth: 1,
85
+ borderColor: `${colors.primary}26`,
86
+ borderRadius: radius.md,
87
+ overflow: 'hidden',
88
+ }}
89
+ >
90
+ <View
91
+ style={{
92
+ backgroundColor: `${colors.primary}0F`,
93
+ paddingHorizontal: spacing.md,
94
+ paddingTop: spacing.md,
95
+ paddingBottom: spacing.sm + 4,
96
+ }}
97
+ >
98
+ <View
99
+ style={{
100
+ flexDirection: 'row',
101
+ alignItems: 'center',
102
+ alignSelf: 'flex-start',
103
+ gap: spacing.sm,
104
+ paddingHorizontal: spacing.sm + 2,
105
+ paddingVertical: 4,
106
+ borderRadius: radius.full,
107
+ backgroundColor: `${colors.primary}1A`,
108
+ }}
109
+ >
110
+ <Icon name="map-pin-check" size={12} color={colors.primary} />
111
+ <MyazaText
112
+ variant="bodySmall"
113
+ color={colors.primary}
114
+ style={{
115
+ fontSize: 11,
116
+ fontWeight: '600',
117
+ letterSpacing: 0.6,
118
+ textTransform: 'uppercase',
119
+ }}
120
+ >
121
+ Address verification
122
+ </MyazaText>
123
+ </View>
124
+ {/* Web's `text-base font-semibold`: the BODY face at 16/600, not a
125
+ heading, so it matches the review card's address line. */}
126
+ <MyazaText variant="body" style={{ marginTop: spacing.sm, fontWeight: '600' }}>
127
+ Let’s confirm your address
128
+ </MyazaText>
129
+ <MyazaText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: 2 }}>
130
+ This address will be verified over the coming days. Your part takes a minute; the rest
131
+ happens on its own.
132
+ </MyazaText>
133
+ </View>
134
+
135
+ <MilestoneTrack milestones={milestonesFor(background)} numbered />
136
+ </View>
137
+
138
+ <View style={{ height: spacing.md }} />
139
+ <IntroDisclosures background={background} />
140
+
141
+ <View style={{ height: spacing.lg }} />
142
+ <MyazaButton
143
+ label="Got it, let’s go"
144
+ onPress={() => store.getState().markAddressIntroSeen()}
145
+ />
146
+ </View>
147
+ );
148
+ }
@@ -0,0 +1,50 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { MyazaText } from '../../components/Typography';
5
+ import { Icon } from '../../components/Icon';
6
+ import { DashedBorder } from '../../components/DashedBorder';
7
+ import { useTheme } from '../../components/runtime';
8
+ import { spacing } from '../../config/theme';
9
+ import type { LatLng } from '../../lib/map-tiles';
10
+
11
+ // The SANDBOX map stand-in (the web SDK's preview placeholder, for test
12
+ // keys): no tiles, no framed page, no vendor loads. The pin lands on the
13
+ // default centre once — what the real map's first idle emits — so Continue
14
+ // stays reachable and the integrator walks the whole flow.
15
+ export function AddressMapStub({
16
+ hasPin,
17
+ onLand,
18
+ defaultCenter,
19
+ height,
20
+ }: {
21
+ hasPin: boolean;
22
+ onLand: (pin: LatLng) => void;
23
+ defaultCenter: LatLng;
24
+ height: number;
25
+ }) {
26
+ useEffect(() => {
27
+ if (!hasPin) onLand(defaultCenter);
28
+ // eslint-disable-next-line react-hooks/exhaustive-deps
29
+ }, [hasPin]);
30
+
31
+ const { colors } = useTheme();
32
+ return (
33
+ <View
34
+ style={{
35
+ height,
36
+ borderRadius: 12,
37
+ alignItems: 'center',
38
+ justifyContent: 'center',
39
+ padding: spacing.lg,
40
+ }}
41
+ >
42
+ <DashedBorder color={colors.border} radius={12} strokeWidth={1} />
43
+ <Icon name="map-pin-house" size={32} color={colors.textSecondary} />
44
+ <View style={{ height: spacing.sm }} />
45
+ <MyazaText variant="bodySmall" color={colors.textSecondary} style={{ textAlign: 'center' }}>
46
+ Applicants place their pin on a live map here. The map loads only for real users.
47
+ </MyazaText>
48
+ </View>
49
+ );
50
+ }
@@ -0,0 +1,178 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { useWindowDimensions, View } from 'react-native';
3
+
4
+ import { spacing } from '../../config/theme';
5
+ import { useKyc, useKycStore, useTheme } from '../../components/runtime';
6
+ import { MyazaText } from '../../components/Typography';
7
+ import { MyazaButton } from '../../components/MyazaButton';
8
+ import { FramedMapPicker } from '../../components/FramedMapPicker';
9
+ import { StickyActions } from '../../components/StickyActions';
10
+ import { displayAddressLine, shouldAskLabelDecision, addressVendorsStubbed } from '../../lib/address-flow';
11
+ import { missingFieldsNudge, missingRequiredAddressFields } from '../../lib/address-field-modes';
12
+ import { claimAutoLocate } from '../../lib/address-current-location';
13
+ import { mapSurfaceHeight } from '../../lib/map-tiles';
14
+ import { useAddressFlow } from './use-address-flow';
15
+ import { AddressMapStub } from './AddressMapStub';
16
+ import { useAddressIntroGate } from './AddressIntroGate';
17
+ import { CurrentLocationRow, LocateChip } from './CurrentLocationRow';
18
+ import { LabelDecisionRow } from './LabelDecisionRow';
19
+ import { DetailsSheet } from './DetailsSheet';
20
+ import { detailValuesOf } from './detail-values';
21
+ import { PinSummaryRow } from './PinSummaryRow';
22
+ import { SkipForNow } from './SkipForNow';
23
+
24
+ /**
25
+ * The PIN step (wire name 'address-collection', kept so older session progress
26
+ * restores cleanly and the server's step-log titles stay meaningful).
27
+ *
28
+ * A big map — sized by mapSurfaceHeight, so a phone keeps room to scroll —
29
+ * with the summary row and the details sheet beneath it, and Continue held at
30
+ * the bottom edge (StickyActions) so the map can never put it out of reach.
31
+ * On KYB this is the WHOLE premises capture, so Continue commits (attest fix
32
+ * included); on an individual flow it advances to the entrance and review
33
+ * steps. Workflow-required details hold Continue and open the sheet on the
34
+ * missing fields rather than pointing at a closed drawer.
35
+ */
36
+ export function AddressPinStep(): React.ReactElement {
37
+ const store = useKycStore();
38
+ const { colors } = useTheme();
39
+ const viewport = useWindowDimensions();
40
+ const flow = useAddressFlow();
41
+ // The framed Google picker when the platform serves one; the OSM picker
42
+ // otherwise (and whenever the page never says ready).
43
+ const mapsFrameUrl = useKyc((s) => s.serverConfig.mapsFrameUrl ?? null);
44
+ // SANDBOX renders the stand-in (the web SDK's vendor-stub rule): verdicts
45
+ // are canned server-side, so a live map on a test key only spends quota.
46
+ const vendorsStubbed = addressVendorsStubbed({
47
+ environment: useKyc((s) => s.serverConfig.environment ?? null),
48
+ });
49
+ const [sheetOpen, setSheetOpen] = useState(false);
50
+ const [missingNudge, setMissingNudge] = useState(false);
51
+ const gate = useAddressIntroGate('address-collection', flow.steps[0]!);
52
+
53
+ const gateShowing = Boolean(gate);
54
+ const noPin = !flow.pin;
55
+ const { startPrefetch, relabelPin, applyCurrentFix } = flow;
56
+ useEffect(() => {
57
+ // The fix warms even under the primer: only the APPLY waits for it, since
58
+ // a pin landing behind the gate would be invisible anyway.
59
+ startPrefetch();
60
+ if (gateShowing) return;
61
+ // A restored pin without its line (a snapshot saved before labels existed):
62
+ // reverse-geocode it once rather than showing raw coordinates.
63
+ relabelPin();
64
+ // Most people are verifying from home, so the map should land on them
65
+ // rather than a country-centre default. Silent by contract, and claimed
66
+ // once per verification: a dismissed or denied prompt must not re-fire
67
+ // every time the person passes back through this step.
68
+ if (!noPin || !claimAutoLocate()) return;
69
+ void applyCurrentFix(undefined, { silent: true });
70
+ }, [gateShowing, noPin, startPrefetch, relabelPin, applyCurrentFix]);
71
+
72
+ if (gate) return gate;
73
+
74
+
75
+
76
+ const address = flow.address;
77
+ const label = address?.label ?? null;
78
+ const askLabel = address ? shouldAskLabelDecision(address) : false;
79
+ // The pin step is the last of the flow on KYB, so its Continue commits.
80
+ const lastInFlow = flow.isBusiness;
81
+ const mapHeight = mapSurfaceHeight(viewport);
82
+
83
+ const missingRequired = missingRequiredAddressFields(flow.cfg, address);
84
+ const handleContinue = () => {
85
+ if (missingRequired.length > 0) {
86
+ setMissingNudge(true);
87
+ setSheetOpen(true);
88
+ return;
89
+ }
90
+ if (lastInFlow) void flow.confirm();
91
+ else flow.goNext();
92
+ };
93
+
94
+ return (
95
+ <>
96
+ <StickyActions
97
+ actions={
98
+ <>
99
+ <MyazaButton
100
+ label="Continue"
101
+ loading={flow.confirming}
102
+ disabled={!flow.pin}
103
+ onPress={handleContinue}
104
+ />
105
+ <SkipForNow requirePin={flow.cfg?.requirePin} onPress={flow.exitForward} />
106
+ </>
107
+ }
108
+ >
109
+ <View>
110
+ {vendorsStubbed ? (
111
+ <AddressMapStub hasPin={Boolean(flow.pin)} onLand={(next) => flow.setPin(next)} defaultCenter={flow.view.center} height={mapHeight} />
112
+ ) : (
113
+ <FramedMapPicker
114
+ frameUrl={mapsFrameUrl}
115
+ value={flow.pin}
116
+ onChange={(next) => flow.setPin(next)}
117
+ defaultCenter={flow.view.center}
118
+ defaultZoom={flow.view.zoom}
119
+ height={mapHeight}
120
+ />
121
+ )}
122
+ {flow.pin ? <LocateChip locating={flow.locating} onPress={() => void flow.locateToPin()} /> : null}
123
+ </View>
124
+
125
+ <View style={{ height: spacing.md }} />
126
+ <PinSummaryRow
127
+ line={address ? displayAddressLine(address) : null}
128
+ labelling={flow.labelling}
129
+ values={detailValuesOf(address)}
130
+ disabled={!flow.pin}
131
+ onPress={() => setSheetOpen(true)}
132
+ />
133
+
134
+ {askLabel && label ? (
135
+ <>
136
+ <View style={{ height: spacing.md }} />
137
+ <LabelDecisionRow label={label} onKeep={flow.keepPickedLabel} onAdopt={flow.adoptPinAddress} />
138
+ </>
139
+ ) : null}
140
+
141
+ {flow.pin ? null : (
142
+ <>
143
+ <View style={{ height: spacing.md }} />
144
+ <CurrentLocationRow hint={flow.currentFix?.label ?? null} locating={flow.locating} onPress={() => void flow.applyCurrentFix()} />
145
+ </>
146
+ )}
147
+
148
+ {flow.error ? (
149
+ <MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.sm }}>
150
+ {flow.error}
151
+ </MyazaText>
152
+ ) : null}
153
+ {missingNudge && missingRequired.length > 0 ? (
154
+ <MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.sm }}>
155
+ {missingFieldsNudge(missingRequired)}
156
+ </MyazaText>
157
+ ) : null}
158
+ </StickyActions>
159
+
160
+ {sheetOpen ? (
161
+ <DetailsSheet
162
+ isBusiness={flow.isBusiness}
163
+ parts={address?.parts ?? null}
164
+ country={flow.country ?? null}
165
+ directionsRequired={flow.cfg?.directions === 'required'}
166
+ addressConfig={flow.cfg}
167
+ values={detailValuesOf(address)}
168
+ disabled={!flow.pin}
169
+ onChange={(patch) => {
170
+ const current = store.getState().address;
171
+ if (current) store.getState().setAddress({ ...current, ...patch });
172
+ }}
173
+ onClose={() => setSheetOpen(false)}
174
+ />
175
+ ) : null}
176
+ </>
177
+ );
178
+ }
@@ -0,0 +1,162 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { radius, spacing } from '../../config/theme';
5
+ import { useKyc, useKycStore, useTheme } from '../../components/runtime';
6
+ import { MyazaText } from '../../components/Typography';
7
+ import { MyazaButton } from '../../components/MyazaButton';
8
+ import { displayAddressLine, addressVendorsStubbed } from '../../lib/address-flow';
9
+ import { useAuthedImage } from '../../lib/authed-image';
10
+ import { missingFieldsNudge, missingRequiredAddressFields } from '../../lib/address-field-modes';
11
+ import { useAddressFlow } from './use-address-flow';
12
+ import { AddressSandboxTabs } from './AddressSandboxTabs';
13
+ import { SkipForNow } from './SkipForNow';
14
+ import { ReviewMapPicture } from './ReviewMapPicture';
15
+ import { HERO, SECOND, ReviewEntranceThumb } from './ReviewEntranceThumbs';
16
+ import { ReviewAddressBand } from './ReviewAddressBand';
17
+
18
+ /**
19
+ * The commit point, as ONE composed card: a read-only summary map with the
20
+ * entrance photo hanging over its bottom edge like a photo clipped to a
21
+ * document, and the address underneath as the card's own heading. Tapping the
22
+ * map jumps back to the pin step. The presence story lives on the intro
23
+ * screen, not here. Workflow-required details the applicant skipped hold
24
+ * Confirm (the pin step's own gate, repeated here as the backstop). The map
25
+ * surface is ReviewMapPicture, the thumbnails ReviewEntranceThumbs.
26
+ */
27
+ export function AddressReviewStep(): React.ReactElement {
28
+ const store = useKycStore();
29
+ const { colors } = useTheme();
30
+ const flow = useAddressFlow();
31
+ const previewUri = useKyc((s) => s.addressPhotoPreview);
32
+ const mapsFrameUrl = useKyc((s) => s.serverConfig.mapsFrameUrl ?? null);
33
+ const vendorsStubbed = addressVendorsStubbed({
34
+ environment: useKyc((s) => s.serverConfig.environment ?? null),
35
+ });
36
+
37
+ // A resumed session can land straight here with a pin that predates the
38
+ // label fields — reverse-geocode it once rather than showing coordinates.
39
+ const { relabelPin } = flow;
40
+ useEffect(() => {
41
+ relabelPin();
42
+ }, [relabelPin]);
43
+
44
+ const address = flow.address;
45
+ const directions = address?.directions.trim() || null;
46
+
47
+ // The framed Street View entrance, drawn through the server: the browser
48
+ // key lives in the framed page, never in this SDK. Fetched with the SDK's
49
+ // bearer by lib/authed-image (an <Image> header is dropped on Android).
50
+ const frame = address?.streetView ?? null;
51
+ const api = useKyc((s) => s.api);
52
+ const frameImage = useAuthedImage(frame ? api.streetViewPreviewSource(frame) : null, 'Street View entrance');
53
+ const frameSource = frameImage.uri ? { uri: frameImage.uri } : null;
54
+ // The map as a picture, through the same bearer; a refused one (the Static
55
+ // API is a separate console enablement) falls back to the live map.
56
+ const mapImage = useAuthedImage(
57
+ flow.pin && !vendorsStubbed ? api.staticMapSource({ ...flow.pin, zoom: 16 }) : null,
58
+ 'Map picture',
59
+ );
60
+ const [pictureFailed, setPictureFailed] = useState(false);
61
+ // An entrance thumbnail whose bytes never arrive must not stay on screen as
62
+ // an empty bordered box hanging over the map (the S24, 2026-09-07); it drops
63
+ // out, and the band's clearance with it.
64
+ const [heroFailed, setHeroFailed] = useState(false);
65
+ const [secondFailed, setSecondFailed] = useState(false);
66
+ const failed = (label: string, error: unknown): void => {
67
+ if (__DEV__) console.warn(`${label} failed to load`, error);
68
+ };
69
+
70
+ // The photo leads when there is one, exactly as on web; the framed view
71
+ // takes the smaller slot beside it, or the big one when it is all there is.
72
+ const hero = previewUri ? { uri: previewUri } : frameSource;
73
+ const second = previewUri && frameSource ? frameSource : null;
74
+ const heroLabel = previewUri ? 'Entrance photo' : 'Street View entrance';
75
+ const heroShown = hero && !heroFailed ? hero : null;
76
+ const secondShown = second && !secondFailed ? second : null;
77
+ const missingRequired = missingRequiredAddressFields(flow.cfg, address);
78
+ const toPin = (): void => store.getState().goToStep('address-collection');
79
+ const line = address ? displayAddressLine(address) : '';
80
+
81
+ return (
82
+ <View>
83
+ <View style={{ borderWidth: 1, borderColor: colors.border, borderRadius: radius.md }}>
84
+ {flow.pin ? (
85
+ // ABOVE the band, which paints a solid background: without this the
86
+ // entrance hanging over the map's edge was covered by it.
87
+ <View style={{ zIndex: 2 }}>
88
+ <ReviewMapPicture
89
+ pin={flow.pin}
90
+ vendorsStubbed={vendorsStubbed}
91
+ mapsFrameUrl={mapsFrameUrl}
92
+ staticMap={mapImage.uri ? { uri: mapImage.uri } : null}
93
+ staticMapFailed={mapImage.failed || pictureFailed}
94
+ onEdit={toPin}
95
+ onPictureFailed={() => setPictureFailed(true)}
96
+ />
97
+ {heroShown ? (
98
+ <ReviewEntranceThumb
99
+ source={heroShown}
100
+ label={heroLabel}
101
+ size={HERO}
102
+ right={16}
103
+ onFailed={(e) => {
104
+ failed(heroLabel, e);
105
+ setHeroFailed(true);
106
+ }}
107
+ />
108
+ ) : null}
109
+ {secondShown ? (
110
+ <ReviewEntranceThumb
111
+ source={secondShown}
112
+ label="Street View entrance"
113
+ size={SECOND}
114
+ right={144}
115
+ onFailed={(e) => {
116
+ failed('Street View entrance', e);
117
+ setSecondFailed(true);
118
+ }}
119
+ />
120
+ ) : null}
121
+ </View>
122
+ ) : null}
123
+
124
+ <ReviewAddressBand
125
+ isBusiness={flow.isBusiness}
126
+ hasAddress={Boolean(address)}
127
+ line={line}
128
+ labelling={flow.labelling}
129
+ directions={directions}
130
+ clearsHero={Boolean(heroShown)}
131
+ onEdit={toPin}
132
+ />
133
+ </View>
134
+
135
+ {flow.error ? (
136
+ <MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.sm }}>
137
+ {flow.error}
138
+ </MyazaText>
139
+ ) : null}
140
+ {missingRequired.length > 0 ? (
141
+ <MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.sm }}>
142
+ {`${missingFieldsNudge(missingRequired)} `}
143
+ <MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }} onPress={toPin}>
144
+ Edit details
145
+ </MyazaText>
146
+ </MyazaText>
147
+ ) : null}
148
+
149
+ <View style={{ height: spacing.lg }} />
150
+ <AddressSandboxTabs />
151
+ <View style={{ height: spacing.md }} />
152
+ <MyazaButton
153
+ label="Confirm address"
154
+ loading={flow.confirming}
155
+ disabled={!flow.pin || missingRequired.length > 0}
156
+ onPress={() => void flow.confirm()}
157
+ />
158
+
159
+ <SkipForNow requirePin={flow.cfg?.requirePin} onPress={flow.exitForward} />
160
+ </View>
161
+ );
162
+ }