@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
@@ -0,0 +1,109 @@
1
+ import React from 'react';
2
+ import { Pressable, View } from 'react-native';
3
+
4
+ import { spacing, radius } from '../config/theme';
5
+ import { useTheme } from '../components/runtime';
6
+ import { MyazaText } from '../components/Typography';
7
+ import { MyazaInput } from '../components/MyazaInput';
8
+ import { emptyKeyPersonOwner, type KeyPersonOwnerEntry } from '../config/keyPeople';
9
+
10
+ const MAX_OWNERS = 10;
11
+
12
+ // Who owns this company.
13
+ //
14
+ // A beneficial owner is a natural person, so a corporate shareholder is a
15
+ // branch of the ownership chain that stops at a legal entity. Where the company
16
+ // is registered somewhere we can look up, the server follows it. Where it is
17
+ // not — a foreign parent, an offshore vehicle — this is the only route to the
18
+ // people above it, and asking is better than recording nothing.
19
+ //
20
+ // Deliberately short: a name and a share. Everything else about them is
21
+ // unknowable to the person filling in this form, and a longer list is one
22
+ // people abandon.
23
+
24
+ export function KeyPersonOwners({
25
+ owners,
26
+ onChange,
27
+ companyName,
28
+ }: {
29
+ owners: KeyPersonOwnerEntry[];
30
+ onChange: (owners: KeyPersonOwnerEntry[]) => void;
31
+ companyName: string;
32
+ }): React.ReactElement {
33
+ const { colors } = useTheme();
34
+ const patch = (index: number, next: Partial<KeyPersonOwnerEntry>) =>
35
+ onChange(owners.map((o, i) => (i === index ? { ...o, ...next } : o)));
36
+
37
+ return (
38
+ <View
39
+ style={{
40
+ borderWidth: 1,
41
+ borderColor: colors.border,
42
+ borderRadius: radius.lg,
43
+ padding: spacing.md,
44
+ }}
45
+ >
46
+ <MyazaText variant="bodySmall" style={{ fontWeight: '600' }}>
47
+ Who owns {companyName.trim() || 'this company'}? (optional)
48
+ </MyazaText>
49
+ <MyazaText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: 2 }}>
50
+ A company cannot verify an identity, so tell us the people behind it if you know them.
51
+ </MyazaText>
52
+
53
+ {owners.map((owner, index) => (
54
+ <View key={index} style={{ marginTop: spacing.sm }}>
55
+ <MyazaInput
56
+ label={`Owner ${index + 1}`}
57
+ value={owner.name}
58
+ onChangeText={(name) => patch(index, { name })}
59
+ placeholder="Full name"
60
+ autoCapitalize="words"
61
+ />
62
+ <View style={{ height: spacing.xs }} />
63
+ <MyazaInput
64
+ label="Their share (optional)"
65
+ value={owner.ownershipPct}
66
+ onChangeText={(ownershipPct) => patch(index, { ownershipPct })}
67
+ placeholder="e.g. 75"
68
+ keyboardType="decimal-pad"
69
+ suffix={
70
+ <MyazaText variant="bodySmall" color={colors.textMuted}>
71
+ %
72
+ </MyazaText>
73
+ }
74
+ />
75
+ <Pressable
76
+ accessibilityRole="button"
77
+ accessibilityLabel={`Remove owner ${index + 1}`}
78
+ onPress={() => onChange(owners.filter((_, i) => i !== index))}
79
+ style={{ paddingVertical: spacing.xs, alignSelf: 'flex-start' }}
80
+ >
81
+ <MyazaText variant="bodySmall" color={colors.error}>
82
+ Remove
83
+ </MyazaText>
84
+ </Pressable>
85
+ </View>
86
+ ))}
87
+
88
+ {owners.length < MAX_OWNERS ? (
89
+ <Pressable
90
+ accessibilityRole="button"
91
+ onPress={() => onChange([...owners, emptyKeyPersonOwner()])}
92
+ style={{
93
+ marginTop: spacing.sm,
94
+ height: 40,
95
+ alignItems: 'center',
96
+ justifyContent: 'center',
97
+ borderRadius: radius.md,
98
+ borderWidth: 1,
99
+ borderColor: colors.border,
100
+ }}
101
+ >
102
+ <MyazaText variant="bodySmall" style={{ fontWeight: '600' }} color={colors.primary}>
103
+ {owners.length === 0 ? 'Add an owner' : 'Add another'}
104
+ </MyazaText>
105
+ </Pressable>
106
+ ) : null}
107
+ </View>
108
+ );
109
+ }
@@ -0,0 +1,68 @@
1
+ import React from 'react';
2
+ import { Pressable, View } from 'react-native';
3
+
4
+ import { MyazaText } from '../components/Typography';
5
+ import { useTheme } from '../components/runtime';
6
+ import { spacing } from '../config/theme';
7
+ import type { KeyPersonRole } from '../types/business';
8
+
9
+ // The representative form's role chips — real classifications, not job titles
10
+ // (those go in the free-text position field). Multi-select over the entry's
11
+ // role SET, with the last representative hat pinned on: unticking it would
12
+ // silently drop the person from the section they are being edited in.
13
+ // Mirrors the web SDK's KeyPersonRoleChips.
14
+
15
+ const REP_ROLES: Array<{ role: KeyPersonRole; label: string }> = [
16
+ { role: 'director', label: 'Director' },
17
+ { role: 'signatory', label: 'Signatory' },
18
+ ];
19
+
20
+ export function KeyPersonRoleChips({
21
+ roles,
22
+ onRoles,
23
+ }: {
24
+ roles: KeyPersonRole[];
25
+ onRoles: (next: KeyPersonRole[]) => void;
26
+ }): React.ReactElement {
27
+ const { colors } = useTheme();
28
+ const repCount = roles.filter((r) => r === 'director' || r === 'signatory').length;
29
+ return (
30
+ <View>
31
+ <MyazaText variant="label" style={{ marginBottom: spacing.sm }}>
32
+ Role
33
+ </MyazaText>
34
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
35
+ {REP_ROLES.map(({ role, label }) => {
36
+ const active = roles.includes(role);
37
+ return (
38
+ <Pressable
39
+ key={role}
40
+ accessibilityRole="button"
41
+ accessibilityState={{ selected: active }}
42
+ onPress={() => {
43
+ if (active && repCount <= 1) return;
44
+ onRoles(active ? roles.filter((r) => r !== role) : [...roles, role]);
45
+ }}
46
+ style={{
47
+ borderRadius: 999,
48
+ borderWidth: 1,
49
+ borderColor: active ? colors.primary : colors.border,
50
+ backgroundColor: active ? colors.primary : 'transparent',
51
+ paddingHorizontal: 14,
52
+ paddingVertical: 7,
53
+ }}
54
+ >
55
+ <MyazaText
56
+ variant="bodyMedium"
57
+ color={active ? colors.onPrimary : colors.textSecondary}
58
+ style={{ fontWeight: '500' }}
59
+ >
60
+ {label}
61
+ </MyazaText>
62
+ </Pressable>
63
+ );
64
+ })}
65
+ </View>
66
+ </View>
67
+ );
68
+ }
@@ -1,53 +1,65 @@
1
1
  import React, { useEffect, useState } from 'react';
2
- import {
3
- Keyboard,
4
- Modal,
5
- Platform,
6
- Pressable,
7
- ScrollView,
8
- useWindowDimensions,
9
- View,
10
- } from 'react-native';
2
+ import { Keyboard, Platform, Pressable, ScrollView, useWindowDimensions, View } from 'react-native';
11
3
 
12
- import { radius, spacing } from '../config/theme';
4
+ import { spacing } from '../config/theme';
13
5
  import { useTheme } from '../components/runtime';
14
6
  import { MyazaText } from '../components/Typography';
15
7
  import { MyazaButton } from '../components/MyazaButton';
16
- import { Icon } from '../components/Icon';
8
+ import { FloatingSheet } from '../components/glass/FloatingSheet';
17
9
  import { KeyPersonForm } from './KeyPersonForm';
18
10
  import { isKeyPersonRowValid, type KeyPersonEntry } from '../config/keyPeople';
11
+ import type { KeyPeopleSection as SectionKey } from '../config/keyPeopleSections';
12
+ import type { KeyPersonRole } from '../types/business';
13
+
14
+ /** What the sheet calls itself, per the section that opened it. */
15
+ const ADD_TITLES: Record<SectionKey, string> = {
16
+ ubos: 'Add a beneficial owner',
17
+ shareholders: 'Add a shareholder',
18
+ representatives: 'Add a representative',
19
+ };
19
20
 
20
21
  // ---------------------------------------------------------------------------
21
22
  // The add/edit key-person sheet. The list step stays a clean stack of summary
22
- // cards; the FORM lives here — slide-up sheet, grab handle, the five fields,
23
- // and a pinned primary action. Editing adds a visually separated destructive
24
- // "Remove this person" beneath the save (never beside it — HIG destructive
25
- // separation).
23
+ // cards; the FORM lives here — the same liquid-glass FloatingSheet every other
24
+ // picker in the SDK rides (handle, glass surface, swipe-down), so the two
25
+ // sheet kinds feel like one system. Editing adds a visually separated
26
+ // destructive "Remove this person" beneath the save (never beside it — HIG
27
+ // destructive separation).
26
28
  //
27
29
  // Keyboard handling mirrors DialCodePicker: the sheet is lifted above the keys
28
30
  // and sized against the space that remains, so the focused field is never
29
31
  // underneath the keyboard.
30
32
  //
31
- // Save is enabled once the draft is valid. A combined-ownership overshoot
32
- // WARNS here but never blocks saving — the fix may live on a different
33
- // person's %, and trapping the user inside this sheet would force them to
34
- // discard their work to go adjust it.
33
+ // Save is enabled once the draft is valid (including a REQUIRED email when the
34
+ // workflow demands one for the role). A combined-ownership overshoot WARNS
35
+ // here but never blocks saving — the fix may live on a different person's %,
36
+ // and trapping the user inside this sheet would force them to discard their
37
+ // work to go adjust it.
35
38
  // ---------------------------------------------------------------------------
36
39
 
37
40
  export function KeyPersonSheet({
38
41
  mode,
42
+ section,
43
+ corporateKyb = false,
39
44
  initial,
40
45
  uboThreshold,
41
46
  otherPctTotal,
47
+ emailRequiredFor,
42
48
  onSave,
43
49
  onRemove,
44
50
  onClose,
45
51
  }: {
46
52
  mode: 'add' | 'edit';
53
+ /** The section whose add-tile or card opened the sheet. */
54
+ section: SectionKey;
55
+ /** The workflow sends corporate shareholders their own KYB application. */
56
+ corporateKyb?: boolean;
47
57
  initial: KeyPersonEntry;
48
58
  uboThreshold?: number;
49
59
  /** Sum of every OTHER person's ownership % — for the combined warning. */
50
60
  otherPctTotal: number;
61
+ /** Roles whose email is mandatory (they are sent a verification link). */
62
+ emailRequiredFor?: ReadonlySet<KeyPersonRole>;
51
63
  onSave: (entry: KeyPersonEntry) => void;
52
64
  /** Edit mode only — removes the person and closes. */
53
65
  onRemove?: () => void;
@@ -82,103 +94,75 @@ export function KeyPersonSheet({
82
94
  const fmtPct = (n: number): string => (Number.isInteger(n) ? String(n) : n.toFixed(1));
83
95
  const combinedPctError =
84
96
  combinedTotal > 100
85
- ? `Combined ownership would be ${fmtPct(combinedTotal)}% — over by ${fmtPct(combinedTotal - 100)}%.`
97
+ ? `Combined ownership would be ${fmtPct(combinedTotal)}%, over by ${fmtPct(combinedTotal - 100)}%.`
86
98
  : null;
87
99
 
88
- const canSave = isKeyPersonRowValid(draft) && draft.name.trim().length >= 2;
100
+ const canSave =
101
+ isKeyPersonRowValid(draft, emailRequiredFor ?? new Set()) && draft.name.trim().length >= 2;
89
102
 
90
103
  return (
91
- <Modal visible transparent animationType="slide" onRequestClose={onClose}>
92
- {/* Same wrapper as MyazaSelect's option sheet: a flex-end backdrop with
93
- the sheet inside it, so the top corners round identically. Tapping
94
- the backdrop dismisses; a tap inside the sheet does not. */}
95
- <Pressable
96
- onPress={onClose}
97
- accessibilityLabel="Close"
104
+ <FloatingSheet
105
+ visible
106
+ onClose={onClose}
107
+ maxHeight={maxHeight}
108
+ // Ride above a raised keyboard — this sheet is a form.
109
+ bottomOffset={keyboard}
110
+ closeLabel="Close person editor"
111
+ >
112
+ <MyazaText
113
+ variant="body"
98
114
  style={{
99
- flex: 1,
100
- backgroundColor: 'rgba(0,0,0,0.45)',
101
- justifyContent: 'flex-end',
115
+ fontWeight: '700',
116
+ paddingHorizontal: spacing.md,
117
+ // Tight under the handle, the way the system sheet titles sit.
118
+ paddingTop: spacing.xs,
119
+ paddingBottom: spacing.sm,
102
120
  }}
103
121
  >
104
- <Pressable
105
- onPress={() => {}}
106
- style={{
107
- maxHeight,
108
- marginBottom: keyboard,
109
- backgroundColor: colors.background,
110
- borderTopLeftRadius: radius.lg,
111
- borderTopRightRadius: radius.lg,
112
- overflow: 'hidden',
113
- }}
114
- >
115
- <View
116
- style={{
117
- alignSelf: 'center',
118
- width: 36,
119
- height: 4,
120
- borderRadius: radius.full,
121
- backgroundColor: colors.border,
122
- marginTop: spacing.sm,
123
- }}
124
- />
125
- <View
126
- style={{
127
- flexDirection: 'row',
128
- alignItems: 'center',
129
- paddingHorizontal: spacing.md,
130
- paddingTop: spacing.sm + 4,
131
- paddingBottom: spacing.sm,
132
- }}
133
- >
134
- <MyazaText variant="body" style={{ flex: 1, fontWeight: '700' }}>
135
- {mode === 'add' ? 'Add a person' : 'Edit person'}
136
- </MyazaText>
137
- <Pressable onPress={onClose} hitSlop={12} accessibilityRole="button" accessibilityLabel="Close">
138
- <Icon name="x" size={20} color={colors.textSecondary} />
139
- </Pressable>
140
- </View>
122
+ {mode === 'add' ? ADD_TITLES[section] : draft.isCorporate ? 'Edit company' : 'Edit person'}
123
+ </MyazaText>
141
124
 
142
- <ScrollView
143
- bounces={false}
144
- keyboardShouldPersistTaps="handled"
145
- contentContainerStyle={{ paddingHorizontal: spacing.md, paddingBottom: spacing.sm }}
146
- >
147
- <KeyPersonForm
148
- entry={draft}
149
- onChange={(patch) => setDraft((d) => ({ ...d, ...patch }))}
150
- uboThreshold={uboThreshold}
151
- combinedPctError={combinedPctError}
152
- />
153
- </ScrollView>
125
+ <ScrollView
126
+ bounces={false}
127
+ keyboardShouldPersistTaps="handled"
128
+ contentContainerStyle={{ paddingHorizontal: spacing.md, paddingBottom: spacing.sm }}
129
+ >
130
+ <KeyPersonForm
131
+ entry={draft}
132
+ section={section}
133
+ corporateKyb={corporateKyb}
134
+ onChange={(patch) => setDraft((d) => ({ ...d, ...patch }))}
135
+ uboThreshold={uboThreshold}
136
+ combinedPctError={combinedPctError}
137
+ emailRequiredFor={emailRequiredFor}
138
+ />
139
+ </ScrollView>
154
140
 
155
- <View style={{ paddingHorizontal: spacing.md, paddingTop: spacing.sm, paddingBottom: spacing.lg }}>
156
- <MyazaButton
157
- label={mode === 'add' ? 'Add person' : 'Save changes'}
158
- onPress={canSave ? () => onSave(draft) : undefined}
159
- disabled={!canSave}
160
- />
161
- {mode === 'edit' && onRemove ? (
162
- <Pressable
163
- onPress={onRemove}
164
- accessibilityRole="button"
165
- accessibilityLabel="Remove this person"
166
- style={({ pressed }) => ({
167
- height: 44,
168
- alignItems: 'center',
169
- justifyContent: 'center',
170
- marginTop: spacing.xs,
171
- opacity: pressed ? 0.7 : 1,
172
- })}
173
- >
174
- <MyazaText variant="bodySmall" color={colors.error} style={{ fontWeight: '600' }}>
175
- Remove this person
176
- </MyazaText>
177
- </Pressable>
178
- ) : null}
179
- </View>
180
- </Pressable>
181
- </Pressable>
182
- </Modal>
141
+ <View style={{ paddingHorizontal: spacing.md, paddingTop: spacing.sm }}>
142
+ <MyazaButton
143
+ label={mode === 'add' ? (draft.isCorporate ? 'Add company' : 'Add person') : 'Save changes'}
144
+ onPress={canSave ? () => onSave(draft) : undefined}
145
+ disabled={!canSave}
146
+ />
147
+ {mode === 'edit' && onRemove ? (
148
+ <Pressable
149
+ onPress={onRemove}
150
+ accessibilityRole="button"
151
+ accessibilityLabel="Remove this person"
152
+ style={({ pressed }) => ({
153
+ height: 44,
154
+ alignItems: 'center',
155
+ justifyContent: 'center',
156
+ marginTop: spacing.xs,
157
+ opacity: pressed ? 0.7 : 1,
158
+ })}
159
+ >
160
+ <MyazaText variant="bodyMedium" color={colors.error} style={{ fontWeight: '600' }}>
161
+ {draft.isCorporate ? 'Remove this company' : 'Remove this person'}
162
+ </MyazaText>
163
+ </Pressable>
164
+ ) : null}
165
+ </View>
166
+ </FloatingSheet>
183
167
  );
184
168
  }
@@ -17,9 +17,21 @@ const GIFS: Record<LivenessChallenge, number> = {
17
17
  smile: require('../assets/liveness/Smile.gif'),
18
18
  };
19
19
 
20
- const SIZE = 96;
20
+ /** The badge on a tall phone; the step hands a smaller size down on a short one
21
+ * (lib/livenessLayout), so the gesture stays on screen beside the circle. */
22
+ const DEFAULT_SIZE = 96;
23
+ const DEFAULT_ICON = 40;
21
24
 
22
- export function LivenessAvatar({ challenge }: { challenge: LivenessChallenge }): React.ReactElement {
25
+ export function LivenessAvatar({
26
+ challenge,
27
+ size = DEFAULT_SIZE,
28
+ iconSize = DEFAULT_ICON,
29
+ }: {
30
+ challenge: LivenessChallenge;
31
+ size?: number;
32
+ iconSize?: number;
33
+ }): React.ReactElement {
34
+ const SIZE = size;
23
35
  const { colors } = useTheme();
24
36
  const [displayed, setDisplayed] = useState<LivenessChallenge>(challenge);
25
37
  const [broken, setBroken] = useState(false);
@@ -55,7 +67,7 @@ export function LivenessAvatar({ challenge }: { challenge: LivenessChallenge }):
55
67
  }}
56
68
  >
57
69
  {broken ? (
58
- <Icon name="scan-face" size={40} color={colors.primary} />
70
+ <Icon name="scan-face" size={iconSize} color={colors.primary} />
59
71
  ) : (
60
72
  <Image
61
73
  source={GIFS[displayed]}