@myazahq/kyc-sdk-react-native 2.4.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 (246) hide show
  1. package/README.md +151 -5
  2. package/android/build.gradle +35 -3
  3. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +77 -0
  4. package/app.plugin.js +89 -8
  5. package/ios/HybridMyazaFaceDetector.swift +16 -3
  6. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +23 -0
  7. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +2 -0
  8. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +9 -0
  9. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +8 -0
  10. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +43 -0
  11. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +17 -0
  12. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  13. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +2 -0
  14. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +31 -0
  15. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +2 -0
  16. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +3 -0
  17. package/package.json +14 -2
  18. package/src/MyazaBiometricAuth.tsx +119 -0
  19. package/src/MyazaKYC.tsx +12 -0
  20. package/src/components/CountryField.tsx +8 -0
  21. package/src/components/CountryRegionPicker.tsx +184 -0
  22. package/src/components/DashedBorder.tsx +55 -0
  23. package/src/components/DialCodePicker.tsx +53 -67
  24. package/src/components/DialCodeRow.tsx +102 -0
  25. package/src/components/FramedMapPicker.tsx +122 -0
  26. package/src/components/GeoBadge.tsx +34 -0
  27. package/src/components/Icon.tsx +2 -176
  28. package/src/components/KycFlow.tsx +60 -32
  29. package/src/components/LineSkeleton.tsx +100 -0
  30. package/src/components/MapChrome.tsx +67 -0
  31. package/src/components/MapPinMarker.tsx +54 -0
  32. package/src/components/MapPinPicker.tsx +179 -0
  33. package/src/components/MilestoneTrack.tsx +155 -0
  34. package/src/components/MultiIdProgress.tsx +107 -0
  35. package/src/components/MyazaInput.tsx +39 -4
  36. package/src/components/MyazaSelect.tsx +2 -1
  37. package/src/components/OptionRow.tsx +22 -3
  38. package/src/components/OwnershipSlider.tsx +102 -0
  39. package/src/components/PhoneNumberInput.tsx +45 -4
  40. package/src/components/PoweredBy.tsx +5 -5
  41. package/src/components/PresenceBlocks.tsx +153 -0
  42. package/src/components/StaggerIn.tsx +63 -0
  43. package/src/components/StepView.tsx +43 -0
  44. package/src/components/StickyActions.tsx +56 -0
  45. package/src/components/dialCodeRows.ts +64 -0
  46. package/src/components/glass/FloatingSheet.tsx +73 -21
  47. package/src/components/glass/GlassSheet.tsx +14 -2
  48. package/src/components/icon-map.ts +176 -0
  49. package/src/components/stepHeaderMeta.tsx +45 -5
  50. package/src/config/addressCollection.ts +125 -0
  51. package/src/config/biometricOptions.ts +100 -0
  52. package/src/config/business.ts +26 -7
  53. package/src/config/businessDetailsValidity.ts +52 -0
  54. package/src/config/businessPrefill.ts +72 -0
  55. package/src/config/businessSteps.ts +20 -3
  56. package/src/config/consentStep.ts +19 -0
  57. package/src/config/keyPeople.ts +111 -7
  58. package/src/config/keyPeopleCorporate.ts +19 -0
  59. package/src/config/keyPeopleOwnerTypes.ts +12 -0
  60. package/src/config/keyPeopleOwners.ts +26 -0
  61. package/src/config/keyPeoplePrefill.ts +138 -0
  62. package/src/config/keyPeopleSectionDefs.ts +65 -0
  63. package/src/config/keyPeopleSections.ts +167 -0
  64. package/src/config/proofOfAddress.ts +54 -11
  65. package/src/config/regions.ts +24 -0
  66. package/src/config/registrationHint.ts +1 -1
  67. package/src/config/stepOrder.ts +100 -6
  68. package/src/config/theme.ts +8 -2
  69. package/src/config/uploadLimits.ts +35 -0
  70. package/src/config/website.ts +36 -0
  71. package/src/config/workflowMerge.ts +16 -5
  72. package/src/emrtd/activeAuth.ts +100 -0
  73. package/src/emrtd/extras.ts +76 -0
  74. package/src/emrtd/files.ts +5 -0
  75. package/src/emrtd/index.ts +1 -0
  76. package/src/emrtd/read.ts +8 -1
  77. package/src/emrtd/session.ts +41 -0
  78. package/src/emrtd/stages.ts +11 -0
  79. package/src/index.ts +54 -1
  80. package/src/lib/address-current-location.ts +171 -0
  81. package/src/lib/address-field-modes.ts +119 -0
  82. package/src/lib/address-flow.ts +172 -0
  83. package/src/lib/address-helpers.ts +48 -0
  84. package/src/lib/address-line.ts +76 -0
  85. package/src/lib/address-pin-move.ts +97 -0
  86. package/src/lib/address-step-recovery.ts +63 -0
  87. package/src/lib/authed-image.ts +78 -0
  88. package/src/lib/biometric-auth.ts +52 -0
  89. package/src/lib/biometric-copy.ts +51 -0
  90. package/src/lib/captureRing.ts +83 -0
  91. package/src/lib/contact-recovery.ts +44 -0
  92. package/src/lib/country-adoption.ts +89 -0
  93. package/src/lib/inferred-country.ts +65 -0
  94. package/src/lib/livenessLayout.ts +49 -0
  95. package/src/lib/map-frame.ts +159 -0
  96. package/src/lib/map-tiles.ts +143 -0
  97. package/src/lib/multi-id.ts +210 -0
  98. package/src/lib/poa-country-gate.ts +27 -0
  99. package/src/lib/resubmit.ts +130 -0
  100. package/src/lib/result-copy.ts +116 -0
  101. package/src/lib/result-wait.ts +53 -0
  102. package/src/lib/review-map-surface.ts +26 -0
  103. package/src/lib/scope.ts +31 -0
  104. package/src/lib/screen-corners.ts +50 -0
  105. package/src/lib/selfie-upload-wait.ts +79 -0
  106. package/src/lib/step-log.ts +24 -3
  107. package/src/lib/street-view-fov.ts +42 -0
  108. package/src/lib/use-multi-id-plan.ts +29 -0
  109. package/src/lib/webview-available.ts +47 -0
  110. package/src/liveness/useLiveness.ts +6 -1
  111. package/src/liveness/useModelReady.ts +65 -0
  112. package/src/liveness/visionCameraFaceDetector.ts +35 -0
  113. package/src/presence/background-math.ts +100 -0
  114. package/src/presence/background-store.ts +82 -0
  115. package/src/presence/background.ts +165 -0
  116. package/src/presence/foreground-service.ts +193 -0
  117. package/src/presence/fs.ts +51 -0
  118. package/src/presence/geofence.ts +41 -0
  119. package/src/presence/math.ts +44 -0
  120. package/src/presence/post.ts +40 -0
  121. package/src/presence/report.ts +87 -0
  122. package/src/presence/sampler.ts +110 -0
  123. package/src/presence/status.ts +92 -0
  124. package/src/presence/store.ts +94 -0
  125. package/src/presence/tier.ts +36 -0
  126. package/src/presence/watch-wait.ts +81 -0
  127. package/src/screens/AddressCountryControl.tsx +115 -0
  128. package/src/screens/ApplicantRoleStep.tsx +9 -5
  129. package/src/screens/BusinessCheckPanel.tsx +89 -0
  130. package/src/screens/BusinessDetailsFields.tsx +112 -0
  131. package/src/screens/BusinessDetailsStep.tsx +133 -145
  132. package/src/screens/BusinessDocumentSlot.tsx +5 -5
  133. package/src/screens/BusinessDocumentsStep.tsx +5 -56
  134. package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
  135. package/src/screens/BusinessPickedCard.tsx +86 -0
  136. package/src/screens/BusinessPickedSection.tsx +51 -0
  137. package/src/screens/BusinessRegistryPickers.tsx +67 -0
  138. package/src/screens/BusinessSandboxToggle.tsx +114 -0
  139. package/src/screens/BusinessSearch.tsx +199 -0
  140. package/src/screens/BusinessSearchResults.tsx +115 -0
  141. package/src/screens/CompanyInfoFields.tsx +91 -51
  142. package/src/screens/ConsentStep.tsx +37 -17
  143. package/src/screens/ContactActions.tsx +54 -0
  144. package/src/screens/ContactDestinationField.tsx +5 -0
  145. package/src/screens/ContactEntryPanel.tsx +75 -0
  146. package/src/screens/ContactVerificationStep.tsx +43 -42
  147. package/src/screens/CountrySelectStep.tsx +13 -147
  148. package/src/screens/IdTypeStep.tsx +12 -5
  149. package/src/screens/KeepLinksSheet.tsx +88 -0
  150. package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
  151. package/src/screens/KeyPeopleAwaitList.tsx +126 -0
  152. package/src/screens/KeyPeoplePending.tsx +154 -0
  153. package/src/screens/KeyPeopleSection.tsx +162 -0
  154. package/src/screens/KeyPeopleSectionsList.tsx +103 -0
  155. package/src/screens/KeyPeopleUboExemption.tsx +72 -0
  156. package/src/screens/KeyPersonCard.tsx +36 -4
  157. package/src/screens/KeyPersonForm.tsx +195 -36
  158. package/src/screens/KeyPersonKindToggle.tsx +68 -0
  159. package/src/screens/KeyPersonOwners.tsx +109 -0
  160. package/src/screens/KeyPersonRoleChips.tsx +68 -0
  161. package/src/screens/KeyPersonSheet.tsx +92 -108
  162. package/src/screens/LivenessAvatar.tsx +15 -3
  163. package/src/screens/LivenessStep.tsx +172 -52
  164. package/src/screens/NfcStep.tsx +14 -0
  165. package/src/screens/PoaDocumentTypeList.tsx +66 -0
  166. package/src/screens/ProofOfAddressParts.tsx +151 -0
  167. package/src/screens/ProofOfAddressStep.tsx +58 -96
  168. package/src/screens/SubmittedBadge.tsx +25 -0
  169. package/src/screens/SubmittedError.tsx +64 -0
  170. package/src/screens/SubmittedResult.tsx +117 -0
  171. package/src/screens/SubmittedStep.tsx +82 -119
  172. package/src/screens/SubmittedSuccess.tsx +127 -0
  173. package/src/screens/SubmittedWaiting.tsx +45 -0
  174. package/src/screens/address/AddressEntranceStep.tsx +161 -0
  175. package/src/screens/address/AddressIntroGate.tsx +148 -0
  176. package/src/screens/address/AddressMapStub.tsx +50 -0
  177. package/src/screens/address/AddressPinStep.tsx +178 -0
  178. package/src/screens/address/AddressReviewStep.tsx +162 -0
  179. package/src/screens/address/AddressSandboxTabs.tsx +158 -0
  180. package/src/screens/address/AddressSearchField.tsx +115 -0
  181. package/src/screens/address/AddressSearchStep.tsx +94 -0
  182. package/src/screens/address/CurrentLocationRow.tsx +135 -0
  183. package/src/screens/address/DetailsSheet.tsx +175 -0
  184. package/src/screens/address/DetailsSheetFields.tsx +184 -0
  185. package/src/screens/address/EntranceDropzone.tsx +178 -0
  186. package/src/screens/address/EntranceFraming.tsx +66 -0
  187. package/src/screens/address/EntrancePills.tsx +60 -0
  188. package/src/screens/address/FramedStreetView.tsx +155 -0
  189. package/src/screens/address/IntroDisclosures.tsx +210 -0
  190. package/src/screens/address/LabelDecisionRow.tsx +113 -0
  191. package/src/screens/address/PinSummaryRow.tsx +105 -0
  192. package/src/screens/address/ReviewAddressBand.tsx +111 -0
  193. package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
  194. package/src/screens/address/ReviewMapPicture.tsx +106 -0
  195. package/src/screens/address/SearchResults.tsx +105 -0
  196. package/src/screens/address/SearchScreen.tsx +176 -0
  197. package/src/screens/address/SkipForNow.tsx +43 -0
  198. package/src/screens/address/StreetViewChrome.tsx +81 -0
  199. package/src/screens/address/detail-values.ts +22 -0
  200. package/src/screens/address/fix-source.ts +27 -0
  201. package/src/screens/address/index.ts +8 -0
  202. package/src/screens/address/meta.ts +44 -0
  203. package/src/screens/address/use-address-flow.ts +200 -0
  204. package/src/screens/address/use-label-pin.ts +80 -0
  205. package/src/screens/address/use-pin-actions.ts +192 -0
  206. package/src/screens/biometric/BiometricAuthFlow.tsx +170 -0
  207. package/src/screens/consent/model.ts +79 -10
  208. package/src/screens/liveness/CaptureRing.tsx +91 -0
  209. package/src/screens/liveness/LivenessCamera.tsx +63 -0
  210. package/src/screens/liveness/LivenessHandover.tsx +43 -0
  211. package/src/screens/liveness/LivenessOutcome.tsx +4 -3
  212. package/src/screens/liveness/SelfiePreview.tsx +13 -4
  213. package/src/screens/liveness/ShutterFlash.tsx +25 -0
  214. package/src/screens/liveness/index.ts +4 -0
  215. package/src/screens/liveness/useSelfieUpload.ts +23 -3
  216. package/src/screens/nfc/NfcReadProgress.tsx +1 -0
  217. package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
  218. package/src/screens/useAddressPhotoAttach.ts +77 -0
  219. package/src/screens/useAwaitingPeople.ts +90 -0
  220. package/src/screens/useBusinessDocumentAttach.ts +93 -0
  221. package/src/screens/usePoaAttach.ts +12 -15
  222. package/src/services/api-biometric.ts +37 -0
  223. package/src/services/api-types-biometric.ts +41 -0
  224. package/src/services/api-types.ts +251 -3
  225. package/src/services/api-verify-types.ts +90 -1
  226. package/src/services/api.ts +237 -3
  227. package/src/services/deviceMetadata.ts +1 -1
  228. package/src/services/errors.ts +10 -2
  229. package/src/services/location.ts +234 -0
  230. package/src/services/uploadErrors.ts +30 -0
  231. package/src/services/workflowGate.ts +8 -0
  232. package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
  233. package/src/store/address.ts +97 -0
  234. package/src/store/businessCheck.ts +113 -0
  235. package/src/store/derive.ts +44 -4
  236. package/src/store/kycStore.ts +352 -16
  237. package/src/store/serverConfig.ts +20 -0
  238. package/src/store/session.ts +227 -0
  239. package/src/store/state.ts +289 -2
  240. package/src/store/submit.ts +80 -13
  241. package/src/store/submitApplicant.ts +5 -11
  242. package/src/types/business.ts +23 -2
  243. package/src/types/config.ts +70 -2
  244. package/src/types/verification.ts +13 -1
  245. package/src/types/workflow.ts +112 -1
  246. package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
@@ -1,17 +1,21 @@
1
- import React, { useCallback, useState } from 'react';
2
- import { ActivityIndicator, Image, Pressable, View } from 'react-native';
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import { View } from 'react-native';
3
3
 
4
- import { radius, spacing } from '../config/theme';
5
- import { useKyc, useKycConfig, useKycStore, useTheme } from '../components/runtime';
4
+ import { spacing } from '../config/theme';
5
+ import { useEffectiveCountry, useKyc, useKycConfig, useKycStore, useTheme } from '../components/runtime';
6
6
  import { MyazaText } from '../components/Typography';
7
7
  import { MyazaButton } from '../components/MyazaButton';
8
- import { Icon } from '../components/Icon';
9
8
  import { MediaSourceSheet } from '../components/MediaSourceSheet';
10
- import { MyazaSelect } from '../components/MyazaSelect';
9
+ import { PoaDocumentTypeList } from './PoaDocumentTypeList';
10
+ import { configScope } from '../lib/scope';
11
11
  import { withRetry } from '../services/retry';
12
12
  import { compressDocumentImage } from '../services/mediaCompress';
13
+ import { uploadFailureMessage } from '../services/uploadErrors';
13
14
  import { poaDocumentTypes, poaMaxAgeDays, poaTypeLabel } from '../config/proofOfAddress';
14
15
  import { usePoaAttach, type PoaPick } from './usePoaAttach';
16
+ import { AddressCountryControl, poaOfferedCountries } from './AddressCountryControl';
17
+ import { poaCountryDeclared } from '../lib/poa-country-gate';
18
+ import { PoaDropzone, PoaUploadedRow } from './ProofOfAddressParts';
15
19
  import type { PoaDocumentType } from '../types/workflow';
16
20
 
17
21
  // ---------------------------------------------------------------------------
@@ -27,10 +31,17 @@ import type { PoaDocumentType } from '../types/workflow';
27
31
  // beyond making sure a file was actually attached.
28
32
  // ---------------------------------------------------------------------------
29
33
 
30
- export function proofOfAddressMeta(maxAgeDays: number): { title: string; description: string } {
34
+ export function proofOfAddressMeta(
35
+ maxAgeDays: number,
36
+ /** Whether the workflow's name rule wants the applicant's name on THIS
37
+ * document. False for e.g. a Nigerian utility bill that names the meter,
38
+ * not the tenant — asking for "your name" there sends people hunting for a
39
+ * document they do not have. */
40
+ nameNeeded = true,
41
+ ): { title: string; description: string } {
31
42
  return {
32
43
  title: 'Proof of address',
33
- description: `Upload a document that shows your name and home address, issued within the last ${maxAgeDays} days.`,
44
+ description: `Upload a document that shows your ${nameNeeded ? 'name and home address' : 'home address'}, issued within the last ${maxAgeDays} days.`,
34
45
  };
35
46
  }
36
47
 
@@ -42,8 +53,23 @@ export function ProofOfAddressStep(): React.ReactElement {
42
53
  const fileName = useKyc((s) => s.poaFileName);
43
54
  const storedType = useKyc((s) => s.poaDocumentType);
44
55
 
45
- const types = poaDocumentTypes(config.proofOfAddress);
56
+ // The kinds on offer follow the country — on the address scope the picker
57
+ // below changes it, and an org may accept different documents per market.
58
+ const country = useEffectiveCountry();
59
+ const types = poaDocumentTypes(config.proofOfAddress, country);
60
+ // The flag on the attachment area: on the address scope only a country the
61
+ // applicant picked (the scope has no seeded country to show), else the
62
+ // flow's effective country.
63
+ const selectedCountry = useKyc((s) => s.selectedCountry);
64
+ const flagCountry = configScope(config) === 'address' ? (selectedCountry ?? null) : (country ?? null);
46
65
  const [selectedType, setSelectedType] = useState<PoaDocumentType>(storedType ?? types[0]!);
66
+ const typesKey = types.join(',');
67
+ useEffect(() => {
68
+ // A country change can withdraw the picked kind; fall back to the first
69
+ // offered rather than submitting a label that country does not accept.
70
+ if (!types.includes(selectedType)) setSelectedType(types[0]!);
71
+ // eslint-disable-next-line react-hooks/exhaustive-deps
72
+ }, [typesKey]);
47
73
  const [busy, setBusy] = useState(false);
48
74
  const [error, setError] = useState<string | null>(null);
49
75
  // The picked file, kept for the thumbnail — the only way a user catches
@@ -66,9 +92,9 @@ export function ProofOfAddressStep(): React.ReactElement {
66
92
  store.getState().api.upload({ uri: finalUri, type: mimeType, name }, 'proof_of_address'),
67
93
  );
68
94
  store.getState().setProofOfAddress(id, selectedType, name);
69
- } catch {
95
+ } catch (e) {
70
96
  setPreview(null);
71
- setError('We could not upload that document. Please try again.');
97
+ setError(uploadFailureMessage(e));
72
98
  } finally {
73
99
  setBusy(false);
74
100
  }
@@ -88,6 +114,14 @@ export function ProofOfAddressStep(): React.ReactElement {
88
114
  }, [store]);
89
115
 
90
116
  const uploaded = Boolean(mediaId) && !busy;
117
+ // The address scope's country is the applicant's declaration and drives the
118
+ // document's market; Continue holds until it is made (the control above
119
+ // asks for it). Never bites elsewhere. See lib/poa-country-gate.ts.
120
+ const countryDeclared = poaCountryDeclared({
121
+ scope: configScope(config),
122
+ selectedCountry,
123
+ offered: poaOfferedCountries(config.proofOfAddress?.countries),
124
+ });
91
125
 
92
126
  return (
93
127
  <View>
@@ -116,109 +150,37 @@ export function ProofOfAddressStep(): React.ReactElement {
116
150
  },
117
151
  ]}
118
152
  />
153
+ <AddressCountryControl />
119
154
  {types.length > 1 ? (
120
155
  <>
121
156
  <MyazaText variant="bodySmall" style={{ fontWeight: '600', marginBottom: spacing.xs }}>
122
157
  Document type
123
158
  </MyazaText>
124
- <MyazaSelect<PoaDocumentType>
159
+ <PoaDocumentTypeList
125
160
  value={selectedType}
126
- sheetTitle="Document type"
127
161
  // Locked once a file is attached: switching the kind afterwards
128
162
  // would mislabel the document already uploaded.
129
- enabled={!uploaded && !busy}
163
+ disabled={uploaded || busy}
130
164
  options={types.map((type) => ({
131
165
  value: type,
132
166
  label: poaTypeLabel(type, config.proofOfAddress),
133
167
  }))}
134
168
  onChange={setSelectedType}
135
169
  />
136
- <View style={{ height: spacing.lg }} />
170
+ <View style={{ height: spacing.sm }} />
137
171
  </>
138
172
  ) : null}
139
173
 
140
174
  {uploaded ? (
141
- <View
142
- style={{
143
- flexDirection: 'row',
144
- alignItems: 'center',
145
- padding: spacing.md,
146
- borderRadius: radius.md,
147
- borderWidth: 1,
148
- borderColor: colors.border,
149
- backgroundColor: colors.backgroundSecondary,
150
- }}
151
- >
152
- {preview && !preview.isPdf ? (
153
- <Image
154
- source={{ uri: preview.uri }}
155
- style={{ width: 48, height: 48, borderRadius: radius.sm }}
156
- resizeMode="cover"
157
- />
158
- ) : (
159
- <View
160
- style={{
161
- width: 48,
162
- height: 48,
163
- borderRadius: radius.sm,
164
- borderWidth: 1,
165
- borderColor: colors.border,
166
- alignItems: 'center',
167
- justifyContent: 'center',
168
- }}
169
- >
170
- <Icon name="file-text" size={22} color={colors.primary} />
171
- </View>
172
- )}
173
- <View style={{ width: spacing.md, flexShrink: 0 }} />
174
- <View style={{ flex: 1, minWidth: 0 }}>
175
- <MyazaText variant="bodySmall" style={{ fontWeight: '600' }} numberOfLines={1}>
176
- {fileName ?? 'Document uploaded'}
177
- </MyazaText>
178
- <MyazaText variant="bodySmall" color={colors.textSecondary} numberOfLines={1}>
179
- {typeLabel}
180
- </MyazaText>
181
- </View>
182
- <Pressable
183
- onPress={remove}
184
- accessibilityRole="button"
185
- accessibilityLabel="Remove document"
186
- hitSlop={8}
187
- >
188
- <Icon name="x" size={18} color={colors.textSecondary} />
189
- </Pressable>
190
- </View>
175
+ <PoaUploadedRow
176
+ preview={preview}
177
+ fileName={fileName}
178
+ typeLabel={typeLabel}
179
+ country={flagCountry}
180
+ onRemove={remove}
181
+ />
191
182
  ) : (
192
- <Pressable
193
- onPress={() => {
194
- if (!busy) void pick();
195
- }}
196
- accessibilityRole="button"
197
- accessibilityLabel={`Upload your ${typeLabel.toLowerCase()}`}
198
- style={{
199
- alignItems: 'center',
200
- justifyContent: 'center',
201
- paddingVertical: spacing.xl,
202
- paddingHorizontal: spacing.lg,
203
- borderRadius: radius.md,
204
- borderWidth: 1.5,
205
- borderStyle: 'dashed',
206
- borderColor: colors.border,
207
- }}
208
- >
209
- {busy ? (
210
- <ActivityIndicator size="small" color={colors.primary} />
211
- ) : (
212
- <Icon name="upload" size={30} color={colors.textSecondary} />
213
- )}
214
- <View style={{ height: spacing.sm }} />
215
- <MyazaText variant="bodySmall" style={{ fontWeight: '600' }}>
216
- {busy ? 'Uploading…' : `Upload your ${typeLabel.toLowerCase()}`}
217
- </MyazaText>
218
- <MyazaText variant="bodySmall" color={colors.textSecondary}>
219
- Photo or PDF, up to 20MB
220
- </MyazaText>
221
- </Pressable>
183
+ <PoaDropzone busy={busy} typeLabel={typeLabel} country={flagCountry} onPress={() => void pick()} />
222
184
  )}
223
185
 
224
186
  {error ? (
@@ -230,7 +192,7 @@ export function ProofOfAddressStep(): React.ReactElement {
230
192
  <View style={{ height: spacing.lg }} />
231
193
  <MyazaButton
232
194
  label="Continue"
233
- disabled={!mediaId || busy}
195
+ disabled={!mediaId || busy || !countryDeclared}
234
196
  onPress={() => store.getState().nextStep()}
235
197
  />
236
198
  </View>
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { radius } from '../config/theme';
5
+
6
+ /** The terminal screens' 88px circular badge: a tinted disc with a 2px ring,
7
+ * holding the outcome icon. Shared by the submitted and result screens. */
8
+ export function Badge({ bg, border, children }: { bg: string; border: string; children: React.ReactNode }): React.ReactElement {
9
+ return (
10
+ <View
11
+ style={{
12
+ width: 88,
13
+ height: 88,
14
+ borderRadius: radius.full,
15
+ backgroundColor: bg,
16
+ borderWidth: 2,
17
+ borderColor: border,
18
+ alignItems: 'center',
19
+ justifyContent: 'center',
20
+ }}
21
+ >
22
+ {children}
23
+ </View>
24
+ );
25
+ }
@@ -0,0 +1,64 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { spacing } from '../config/theme';
5
+ import type { KYCError } from '../types/verification';
6
+ import { useTheme } from '../components/runtime';
7
+ import { MyazaText } from '../components/Typography';
8
+ import { MyazaButton } from '../components/MyazaButton';
9
+ import { MyazaAlert } from '../components/MyazaAlert';
10
+ import { Icon } from '../components/Icon';
11
+ import { Badge } from './SubmittedBadge';
12
+
13
+ // The submitted step's error screen (split from SubmittedStep, 200-line
14
+ // rule). `onRetry` is null when the failure is not recoverable in-flow (an
15
+ // exhausted balance, a refused key); the step decides, this only renders.
16
+
17
+ const ERROR_TITLES: Record<string, string> = {
18
+ insufficient_credits: 'Credits Exhausted',
19
+ invalid_api_key: 'Authentication Failed',
20
+ feature_disabled: 'Verification Unavailable',
21
+ upload_failed: 'Upload Failed',
22
+ network_error: 'Connection Failed',
23
+ };
24
+
25
+ export function SubmittedError({
26
+ error,
27
+ onRetry,
28
+ onClose,
29
+ }: {
30
+ error: KYCError;
31
+ onRetry: (() => void) | null;
32
+ onClose: () => void;
33
+ }): React.ReactElement {
34
+ const { colors } = useTheme();
35
+ const title = ERROR_TITLES[error.code] ?? 'Submission Failed';
36
+ return (
37
+ <View style={{ flex: 1, justifyContent: 'space-between' }}>
38
+ <View style={{ alignItems: 'center' }}>
39
+ <View style={{ height: spacing.xl }} />
40
+ <Badge bg={colors.errorBg} border={`${colors.error}4D`}>
41
+ <Icon name="alert" size={44} color={colors.error} />
42
+ </Badge>
43
+ <View style={{ height: spacing.lg }} />
44
+ <MyazaText variant="heading1" style={{ textAlign: 'center' }}>
45
+ {title}
46
+ </MyazaText>
47
+ <View style={{ height: spacing.md }} />
48
+ <View style={{ width: '100%', paddingHorizontal: spacing.md }}>
49
+ <MyazaAlert variant="error" title="What happened" message={error.message} />
50
+ </View>
51
+ </View>
52
+ <View style={{ flexDirection: 'row', gap: spacing.md }}>
53
+ {onRetry ? (
54
+ <View style={{ flex: 1 }}>
55
+ <MyazaButton label="Try Again" variant="outline" leadingIcon="refresh" onPress={onRetry} />
56
+ </View>
57
+ ) : null}
58
+ <View style={{ flex: 1 }}>
59
+ <MyazaButton label="Close" onPress={onClose} />
60
+ </View>
61
+ </View>
62
+ </View>
63
+ );
64
+ }
@@ -0,0 +1,117 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { spacing } from '../config/theme';
5
+ import { 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 { awaitVerificationOutcome, type VerificationOutcome } from '../lib/result-wait';
10
+ import { describeOutcome, describeWaiting } from '../lib/result-copy';
11
+ import { biometricCopyFor } from '../lib/biometric-copy';
12
+ import { configScope } from '../lib/scope';
13
+ import { Badge } from './SubmittedBadge';
14
+ import { SubmittedWaiting } from './SubmittedWaiting';
15
+ import { StaggerIn } from '../components/StaggerIn';
16
+
17
+ // ─── The result screen (a flow that waits for its verdict) ──────────────────
18
+ //
19
+ // A biometric re-authentication delivered in the flow ('both', the default, or 'app')
20
+ // is answered NOW or it is useless. This screen is mounted from the submitted
21
+ // step's FIRST render and shows one loading screen through the selfie upload,
22
+ // the submission (`verificationId` is null until it lands) and the status
23
+ // poll, then the verdict, firing `onResult` once. The wait has a budget: past
24
+ // it the screen says so and the webhook remains the record, exactly as on
25
+ // every other flow. `showDone` is the `doneButton` option: off when the host
26
+ // app dismisses the flow itself from `onResult`.
27
+
28
+ export function SubmittedResult({
29
+ verificationId,
30
+ retry,
31
+ showDone,
32
+ onClose,
33
+ }: {
34
+ verificationId: string | null;
35
+ retry: { attempt: number; total: number } | null;
36
+ showDone: boolean;
37
+ onClose: () => void;
38
+ }): React.ReactElement {
39
+ const { colors } = useTheme();
40
+ const config = useKycConfig();
41
+ const store = useKycStore();
42
+ const [outcome, setOutcome] = useState<VerificationOutcome | null>(null);
43
+
44
+ useEffect(() => {
45
+ if (!verificationId) return undefined;
46
+ let alive = true;
47
+ const api = store.getState().api;
48
+ void awaitVerificationOutcome({
49
+ fetchStatus: () => api.status(verificationId).catch(() => null),
50
+ }).then((settled) => {
51
+ if (!alive) return;
52
+ setOutcome(settled);
53
+ if (settled.kind === 'settled') {
54
+ config.onResult?.({
55
+ verificationId,
56
+ status: settled.status,
57
+ reason: settled.reason,
58
+ reasonCode: settled.reasonCode,
59
+ });
60
+ }
61
+ });
62
+ return () => {
63
+ alive = false;
64
+ };
65
+ // The wait is tied to the verification, not to the callback identity.
66
+ // eslint-disable-next-line react-hooks/exhaustive-deps
67
+ }, [verificationId]);
68
+
69
+ // The org's own words for these screens, tokens filled (null = the defaults).
70
+ const words = biometricCopyFor(config);
71
+ if (!outcome) {
72
+ const copy = describeWaiting({ scope: configScope(config), waitsForResult: true, retry, override: words.waiting });
73
+ return <SubmittedWaiting title={copy.title} description={copy.description} retrying={retry != null} />;
74
+ }
75
+
76
+ const copy = describeOutcome(outcome, words);
77
+ const palette =
78
+ copy.tone === 'success'
79
+ ? { bg: colors.successBg, fg: colors.success, icon: 'check' as const }
80
+ : copy.tone === 'error'
81
+ ? { bg: colors.errorBg, fg: colors.error, icon: 'alert' as const }
82
+ : { bg: `${colors.primary}14`, fg: colors.primary, icon: 'info' as const };
83
+
84
+ return (
85
+ <View style={{ flex: 1, justifyContent: 'space-between' }}>
86
+ <View style={{ alignItems: 'center' }}>
87
+ <View style={{ height: spacing.xl }} />
88
+ {/* The verdict lands in the order Flutter's does: badge, title,
89
+ description, then Done (delays 0 / 200 / 320 / 600 ms). */}
90
+ <StaggerIn>
91
+ <Badge bg={palette.bg} border={`${palette.fg}4D`}>
92
+ <Icon name={palette.icon} size={44} color={palette.fg} />
93
+ </Badge>
94
+ </StaggerIn>
95
+ <View style={{ height: spacing.lg }} />
96
+ <StaggerIn delayMs={200}>
97
+ <MyazaText variant="heading1" style={{ textAlign: 'center' }}>
98
+ {copy.title}
99
+ </MyazaText>
100
+ </StaggerIn>
101
+ <View style={{ height: spacing.sm }} />
102
+ <StaggerIn delayMs={320}>
103
+ <MyazaText variant="bodyMedium" style={{ textAlign: 'center', paddingHorizontal: spacing.lg }}>
104
+ {copy.description}
105
+ </MyazaText>
106
+ </StaggerIn>
107
+ </View>
108
+ {showDone ? (
109
+ <StaggerIn delayMs={600}>
110
+ <MyazaButton label="Done" onPress={onClose} />
111
+ </StaggerIn>
112
+ ) : (
113
+ <View />
114
+ )}
115
+ </View>
116
+ );
117
+ }