@myazahq/kyc-sdk-react-native 2.4.0 → 2.5.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 (117) hide show
  1. package/android/build.gradle +35 -3
  2. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +75 -0
  3. package/ios/HybridMyazaFaceDetector.swift +8 -0
  4. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +23 -0
  5. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +2 -0
  6. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +9 -0
  7. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +8 -0
  8. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +43 -0
  9. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +17 -0
  10. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  11. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +2 -0
  12. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +31 -0
  13. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +2 -0
  14. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +3 -0
  15. package/package.json +1 -1
  16. package/src/MyazaKYC.tsx +12 -0
  17. package/src/components/DashedBorder.tsx +55 -0
  18. package/src/components/DialCodePicker.tsx +14 -7
  19. package/src/components/Icon.tsx +9 -0
  20. package/src/components/MultiIdProgress.tsx +107 -0
  21. package/src/components/MyazaInput.tsx +23 -4
  22. package/src/components/MyazaSelect.tsx +2 -1
  23. package/src/components/OwnershipSlider.tsx +102 -0
  24. package/src/components/PhoneNumberInput.tsx +37 -4
  25. package/src/components/StaggerIn.tsx +63 -0
  26. package/src/components/StepView.tsx +26 -0
  27. package/src/components/glass/FloatingSheet.tsx +73 -21
  28. package/src/components/glass/GlassSheet.tsx +14 -2
  29. package/src/components/stepHeaderMeta.tsx +3 -1
  30. package/src/config/business.ts +6 -6
  31. package/src/config/businessDetailsValidity.ts +52 -0
  32. package/src/config/businessPrefill.ts +72 -0
  33. package/src/config/businessSteps.ts +14 -3
  34. package/src/config/keyPeople.ts +111 -7
  35. package/src/config/keyPeopleCorporate.ts +19 -0
  36. package/src/config/keyPeopleOwnerTypes.ts +12 -0
  37. package/src/config/keyPeopleOwners.ts +26 -0
  38. package/src/config/keyPeoplePrefill.ts +138 -0
  39. package/src/config/keyPeopleSectionDefs.ts +65 -0
  40. package/src/config/keyPeopleSections.ts +167 -0
  41. package/src/config/registrationHint.ts +1 -1
  42. package/src/config/stepOrder.ts +24 -4
  43. package/src/config/theme.ts +8 -2
  44. package/src/config/website.ts +36 -0
  45. package/src/config/workflowMerge.ts +5 -0
  46. package/src/emrtd/activeAuth.ts +100 -0
  47. package/src/emrtd/extras.ts +76 -0
  48. package/src/emrtd/files.ts +5 -0
  49. package/src/emrtd/index.ts +1 -0
  50. package/src/emrtd/read.ts +8 -1
  51. package/src/emrtd/session.ts +41 -0
  52. package/src/emrtd/stages.ts +11 -0
  53. package/src/lib/contact-recovery.ts +44 -0
  54. package/src/lib/multi-id.ts +210 -0
  55. package/src/lib/resubmit.ts +130 -0
  56. package/src/lib/screen-corners.ts +50 -0
  57. package/src/lib/step-log.ts +24 -3
  58. package/src/lib/use-multi-id-plan.ts +29 -0
  59. package/src/liveness/useModelReady.ts +65 -0
  60. package/src/liveness/visionCameraFaceDetector.ts +35 -0
  61. package/src/screens/ApplicantRoleStep.tsx +9 -5
  62. package/src/screens/BusinessCheckPanel.tsx +89 -0
  63. package/src/screens/BusinessDetailsFields.tsx +109 -0
  64. package/src/screens/BusinessDetailsStep.tsx +131 -145
  65. package/src/screens/BusinessDocumentSlot.tsx +2 -3
  66. package/src/screens/BusinessDocumentsStep.tsx +1 -1
  67. package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
  68. package/src/screens/BusinessPickedCard.tsx +86 -0
  69. package/src/screens/BusinessPickedSection.tsx +51 -0
  70. package/src/screens/BusinessRegistryPickers.tsx +67 -0
  71. package/src/screens/BusinessSandboxToggle.tsx +114 -0
  72. package/src/screens/BusinessSearch.tsx +199 -0
  73. package/src/screens/BusinessSearchResults.tsx +115 -0
  74. package/src/screens/CompanyInfoFields.tsx +86 -51
  75. package/src/screens/ConsentStep.tsx +31 -0
  76. package/src/screens/ContactDestinationField.tsx +1 -0
  77. package/src/screens/ContactVerificationStep.tsx +34 -3
  78. package/src/screens/IdTypeStep.tsx +12 -5
  79. package/src/screens/KeepLinksSheet.tsx +88 -0
  80. package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
  81. package/src/screens/KeyPeopleAwaitList.tsx +126 -0
  82. package/src/screens/KeyPeoplePending.tsx +154 -0
  83. package/src/screens/KeyPeopleSection.tsx +162 -0
  84. package/src/screens/KeyPeopleSectionsList.tsx +103 -0
  85. package/src/screens/KeyPeopleUboExemption.tsx +72 -0
  86. package/src/screens/KeyPersonCard.tsx +36 -4
  87. package/src/screens/KeyPersonForm.tsx +195 -36
  88. package/src/screens/KeyPersonKindToggle.tsx +68 -0
  89. package/src/screens/KeyPersonOwners.tsx +109 -0
  90. package/src/screens/KeyPersonRoleChips.tsx +68 -0
  91. package/src/screens/KeyPersonSheet.tsx +92 -108
  92. package/src/screens/LivenessStep.tsx +18 -0
  93. package/src/screens/NfcStep.tsx +14 -0
  94. package/src/screens/ProofOfAddressStep.tsx +6 -3
  95. package/src/screens/SubmittedStep.tsx +66 -10
  96. package/src/screens/nfc/NfcReadProgress.tsx +1 -0
  97. package/src/screens/useAwaitingPeople.ts +90 -0
  98. package/src/services/api-types.ts +158 -2
  99. package/src/services/api-verify-types.ts +53 -1
  100. package/src/services/api.ts +104 -0
  101. package/src/services/deviceMetadata.ts +1 -1
  102. package/src/services/errors.ts +10 -2
  103. package/src/services/workflowGate.ts +1 -0
  104. package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
  105. package/src/store/businessCheck.ts +113 -0
  106. package/src/store/derive.ts +15 -0
  107. package/src/store/kycStore.ts +241 -14
  108. package/src/store/serverConfig.ts +2 -0
  109. package/src/store/session.ts +208 -0
  110. package/src/store/state.ts +155 -2
  111. package/src/store/submit.ts +63 -13
  112. package/src/store/submitApplicant.ts +5 -11
  113. package/src/types/business.ts +23 -2
  114. package/src/types/config.ts +22 -1
  115. package/src/types/verification.ts +1 -1
  116. package/src/types/workflow.ts +15 -0
  117. package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
@@ -0,0 +1,162 @@
1
+ import React from 'react';
2
+ import { Pressable, View } from 'react-native';
3
+
4
+ import { DashedBorder } from '../components/DashedBorder';
5
+ import { Icon } from '../components/Icon';
6
+ import { MyazaText } from '../components/Typography';
7
+ import { useTheme } from '../components/runtime';
8
+ import { radius, spacing } from '../config/theme';
9
+ import { KEY_PERSON_ROLE_LABELS, type KeyPersonEntry } from '../config/keyPeople';
10
+ import { SECTION_ROLE, type KeyPeopleSection as SectionKey } from '../config/keyPeopleSections';
11
+ import { KeyPersonCard } from './KeyPersonCard';
12
+ import type { KeyPersonRole } from '../types/business';
13
+
14
+ // One section of the key-people step — heading, plain-language definition,
15
+ // member cards, quick-add chips for people already entered elsewhere, and the
16
+ // dashed add-tile. Sections are VIEWS over one shared list
17
+ // (keyPeopleSections.ts). Mirrors the web SDK's KeyPeopleSection 1:1.
18
+
19
+ export function KeyPeopleSection({
20
+ section,
21
+ title,
22
+ description,
23
+ rows,
24
+ members,
25
+ quickAdd,
26
+ emailRequiredFor,
27
+ addLabel,
28
+ canAdd,
29
+ onAdd,
30
+ onEdit,
31
+ onRemove,
32
+ onQuickAdd,
33
+ children,
34
+ }: {
35
+ section: SectionKey;
36
+ title: string;
37
+ description: string;
38
+ rows: KeyPersonEntry[];
39
+ members: number[];
40
+ quickAdd: number[];
41
+ emailRequiredFor: ReadonlySet<KeyPersonRole>;
42
+ addLabel: string;
43
+ canAdd: boolean;
44
+ onAdd: () => void;
45
+ onEdit: (index: number) => void;
46
+ onRemove: (index: number) => void;
47
+ onQuickAdd: (index: number) => void;
48
+ children?: React.ReactNode;
49
+ }): React.ReactElement {
50
+ const { colors } = useTheme();
51
+ return (
52
+ <View>
53
+ <MyazaText variant="heading3">{title}</MyazaText>
54
+ <MyazaText variant="bodyMedium" color={colors.textSecondary} style={{ marginTop: 2 }}>
55
+ {description}
56
+ </MyazaText>
57
+
58
+ {members.length > 0 ? (
59
+ <View style={{ marginTop: spacing.sm + 4 }}>
60
+ {members.map((index) => (
61
+ <KeyPersonCard
62
+ key={index}
63
+ entry={rows[index]!}
64
+ emailRequiredFor={emailRequiredFor}
65
+ // The card names the hat THIS section is about, so the same
66
+ // person reads "Beneficial owner" here and "Director" there.
67
+ roleLabel={KEY_PERSON_ROLE_LABELS[SECTION_ROLE[section]]}
68
+ onPress={() => onEdit(index)}
69
+ onRemove={() => onRemove(index)}
70
+ />
71
+ ))}
72
+ </View>
73
+ ) : null}
74
+
75
+ {quickAdd.length > 0 && canAdd ? (
76
+ <View style={{ marginTop: spacing.sm + 4 }}>
77
+ <MyazaText
78
+ variant="bodySmall"
79
+ color={colors.textSecondary}
80
+ style={{ textTransform: 'uppercase', letterSpacing: 0.6, fontWeight: '600' }}
81
+ >
82
+ Quick add from people you already entered
83
+ </MyazaText>
84
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: spacing.sm }}>
85
+ {quickAdd.map((index) => {
86
+ const row = rows[index]!;
87
+ return (
88
+ <Pressable
89
+ key={index}
90
+ accessibilityRole="button"
91
+ onPress={() => onQuickAdd(index)}
92
+ style={{
93
+ flexDirection: 'row',
94
+ alignItems: 'center',
95
+ gap: 8,
96
+ borderWidth: 1,
97
+ borderColor: colors.border,
98
+ borderRadius: 999,
99
+ paddingVertical: 6,
100
+ paddingLeft: 12,
101
+ paddingRight: 8,
102
+ }}
103
+ >
104
+ <Icon
105
+ name={row.isCorporate ? 'building-2' : 'user'}
106
+ size={16}
107
+ color={colors.textSecondary}
108
+ />
109
+ <MyazaText
110
+ variant="bodyMedium"
111
+ color={colors.textDark}
112
+ numberOfLines={1}
113
+ style={{ fontWeight: '500', maxWidth: 160 }}
114
+ >
115
+ {row.name.trim()}
116
+ </MyazaText>
117
+ <View
118
+ style={{
119
+ width: 20,
120
+ height: 20,
121
+ borderRadius: 10,
122
+ backgroundColor: colors.primary100,
123
+ alignItems: 'center',
124
+ justifyContent: 'center',
125
+ }}
126
+ >
127
+ <Icon name="plus" size={12} color={colors.primary} />
128
+ </View>
129
+ </Pressable>
130
+ );
131
+ })}
132
+ </View>
133
+ </View>
134
+ ) : null}
135
+
136
+ {canAdd ? (
137
+ <Pressable
138
+ accessibilityRole="button"
139
+ onPress={onAdd}
140
+ style={{
141
+ marginTop: spacing.sm + 4,
142
+ borderRadius: radius.sm,
143
+ paddingVertical: 14,
144
+ alignItems: 'center',
145
+ justifyContent: 'center',
146
+ flexDirection: 'row',
147
+ gap: 6,
148
+ }}
149
+ >
150
+ {/* The same tight dashed language as the document slots. */}
151
+ <DashedBorder color={colors.border} radius={radius.sm} />
152
+ <Icon name="plus" size={16} color={colors.primary} />
153
+ <MyazaText variant="bodyMedium" color={colors.primary} style={{ fontWeight: '600' }}>
154
+ {addLabel}
155
+ </MyazaText>
156
+ </Pressable>
157
+ ) : null}
158
+
159
+ {children}
160
+ </View>
161
+ );
162
+ }
@@ -0,0 +1,103 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { useTheme } from '../components/runtime';
5
+ import { spacing } from '../config/theme';
6
+ import {
7
+ grantRole,
8
+ quickAddCandidates,
9
+ sectionMembers,
10
+ withoutSection,
11
+ type KeyPeopleSection as SectionKey,
12
+ } from '../config/keyPeopleSections';
13
+ import type { KeyPeopleSectionDef } from '../config/keyPeopleSectionDefs';
14
+ import { KeyPeopleSection } from './KeyPeopleSection';
15
+ import { KeyPeopleUboExemption } from './KeyPeopleUboExemption';
16
+ import type { KeyPersonEntry } from '../config/keyPeople';
17
+ import type { KeyPersonRole } from '../types/business';
18
+
19
+ // The stacked sections with their shared-list plumbing: quick-add grants an
20
+ // existing person the section's hat, and the card's X takes them out of that
21
+ // section — dropping just the hat when membership rested on it, or the whole
22
+ // person when a declared stake (or a last remaining hat) is what holds them
23
+ // there. Split from BusinessKeyPeopleStep (200-line rule); mirrors the web
24
+ // SDK's KeyPeopleSectionsList 1:1.
25
+
26
+ export function KeyPeopleSectionsList({
27
+ sections,
28
+ rows,
29
+ threshold,
30
+ emailRequiredFor,
31
+ uboUnidentifiable,
32
+ canAdd,
33
+ onRows,
34
+ onSheet,
35
+ onExemption,
36
+ }: {
37
+ sections: KeyPeopleSectionDef[];
38
+ rows: KeyPersonEntry[];
39
+ threshold: number;
40
+ emailRequiredFor: ReadonlySet<KeyPersonRole>;
41
+ uboUnidentifiable: boolean;
42
+ canAdd: boolean;
43
+ onRows: (next: KeyPersonEntry[]) => void;
44
+ onSheet: (sheet: { mode: 'add' | 'edit'; section: SectionKey; index?: number }) => void;
45
+ onExemption: (next: boolean) => void;
46
+ }): React.ReactElement {
47
+ const { colors } = useTheme();
48
+ const members = sectionMembers(rows, threshold);
49
+
50
+ const removeFromSection = (section: SectionKey, index: number): void => {
51
+ const next = withoutSection(rows[index]!, section, threshold);
52
+ onRows(
53
+ next === null
54
+ ? rows.filter((_, i) => i !== index)
55
+ : rows.map((row, i) => (i === index ? next : row)),
56
+ );
57
+ };
58
+
59
+ return (
60
+ <View>
61
+ {sections.map(({ key, title, description, addLabel }, i) => (
62
+ <React.Fragment key={key}>
63
+ {i > 0 ? (
64
+ <View
65
+ style={{
66
+ borderTopWidth: 1,
67
+ borderTopColor: colors.border,
68
+ marginVertical: spacing.lg,
69
+ }}
70
+ />
71
+ ) : null}
72
+ <KeyPeopleSection
73
+ section={key}
74
+ title={title}
75
+ description={description}
76
+ rows={rows}
77
+ members={members[key]}
78
+ quickAdd={
79
+ key === 'ubos' && uboUnidentifiable ? [] : quickAddCandidates(rows, key, threshold)
80
+ }
81
+ emailRequiredFor={emailRequiredFor}
82
+ addLabel={addLabel}
83
+ canAdd={canAdd && !(key === 'ubos' && uboUnidentifiable)}
84
+ onAdd={() => onSheet({ mode: 'add', section: key })}
85
+ onEdit={(index) => onSheet({ mode: 'edit', section: key, index })}
86
+ onRemove={(index) => removeFromSection(key, index)}
87
+ onQuickAdd={(index) =>
88
+ onRows(rows.map((row, i2) => (i2 === index ? grantRole(row, key) : row)))
89
+ }
90
+ >
91
+ {key === 'ubos' ? (
92
+ <KeyPeopleUboExemption
93
+ checked={uboUnidentifiable}
94
+ hasUbos={members.ubos.length > 0}
95
+ onChange={onExemption}
96
+ />
97
+ ) : null}
98
+ </KeyPeopleSection>
99
+ </React.Fragment>
100
+ ))}
101
+ </View>
102
+ );
103
+ }
@@ -0,0 +1,72 @@
1
+ import React from 'react';
2
+ import { Pressable, View } from 'react-native';
3
+
4
+ import { Icon } from '../components/Icon';
5
+ import { MyazaText } from '../components/Typography';
6
+ import { useTheme } from '../components/runtime';
7
+ import { spacing } from '../config/theme';
8
+
9
+ // The FATF fallback, attested: some companies genuinely have no natural
10
+ // person who qualifies as a UBO (listed companies, complex trusts, nominee
11
+ // arrangements), and without this box the applicant's only moves were to
12
+ // stall or to invent one. It is an attestation the org can branch on, never a
13
+ // verdict — and it is disabled the moment a UBO is listed, because the two
14
+ // claims contradict. Mirrors the web SDK's KeyPeopleUboExemption.
15
+
16
+ export function KeyPeopleUboExemption({
17
+ checked,
18
+ hasUbos,
19
+ onChange,
20
+ }: {
21
+ checked: boolean;
22
+ hasUbos: boolean;
23
+ onChange: (checked: boolean) => void;
24
+ }): React.ReactElement {
25
+ const { colors } = useTheme();
26
+ return (
27
+ <View style={{ marginTop: spacing.sm + 4 }}>
28
+ <Pressable
29
+ accessibilityRole="checkbox"
30
+ accessibilityState={{ checked, disabled: hasUbos }}
31
+ disabled={hasUbos}
32
+ onPress={() => onChange(!checked)}
33
+ style={{
34
+ flexDirection: 'row',
35
+ alignItems: 'flex-start',
36
+ gap: 12,
37
+ opacity: hasUbos ? 0.5 : 1,
38
+ }}
39
+ >
40
+ <View
41
+ style={{
42
+ width: 20,
43
+ height: 20,
44
+ marginTop: 1,
45
+ borderRadius: 6,
46
+ borderWidth: 1.5,
47
+ borderColor: checked ? colors.primary : colors.border,
48
+ backgroundColor: checked ? colors.primary : 'transparent',
49
+ alignItems: 'center',
50
+ justifyContent: 'center',
51
+ }}
52
+ >
53
+ {checked ? <Icon name="check" size={13} color={colors.onPrimary} /> : null}
54
+ </View>
55
+ <MyazaText variant="bodyMedium" color={colors.textSecondary} style={{ flex: 1 }}>
56
+ UBOs cannot be identified due to public share structures, complex trusts or nominee
57
+ arrangements.
58
+ </MyazaText>
59
+ </Pressable>
60
+ {checked && !hasUbos ? (
61
+ <MyazaText
62
+ variant="bodySmall"
63
+ color={colors.textSecondary}
64
+ style={{ marginTop: 6, paddingLeft: 32 }}
65
+ >
66
+ We will record this with the application; a senior person is still identified through
67
+ the applicant's own verification.
68
+ </MyazaText>
69
+ ) : null}
70
+ </View>
71
+ );
72
+ }
@@ -7,12 +7,14 @@ import { MyazaText } from '../components/Typography';
7
7
  import { Icon } from '../components/Icon';
8
8
  import { CountryFlag } from '../components/CountryFlag';
9
9
  import {
10
+ rowNeedsEmail,
10
11
  KEY_PERSON_ROLE_LABELS,
11
12
  initialsOf,
12
13
  isKeyPersonRowValid,
13
14
  type KeyPersonEntry,
14
15
  } from '../config/keyPeople';
15
16
  import { regionCountryName } from '../config/regions';
17
+ import type { KeyPersonRole } from '../types/business';
16
18
 
17
19
  // ---------------------------------------------------------------------------
18
20
  // One saved key person, summarised — monogram avatar with their ID-issuing
@@ -27,20 +29,39 @@ import { regionCountryName } from '../config/regions';
27
29
 
28
30
  export function KeyPersonCard({
29
31
  entry,
32
+ emailRequiredFor,
30
33
  onPress,
34
+ roleLabel,
35
+ onRemove,
31
36
  }: {
32
37
  entry: KeyPersonEntry;
38
+ emailRequiredFor?: ReadonlySet<KeyPersonRole>;
33
39
  onPress: () => void;
40
+ /** Names the hat the HOSTING section is about (sectioned step). */
41
+ roleLabel?: string;
42
+ /** The sectioned step's X: take this person out of that section. */
43
+ onRemove?: () => void;
34
44
  }): React.ReactElement {
35
45
  const { colors } = useTheme();
36
46
  const name = entry.name.trim() || 'Unnamed person';
37
47
  const country = entry.country.trim() ? entry.country.trim().toUpperCase() : null;
38
48
  const pct = entry.ownershipPct.trim();
39
49
  // A row persisted by the old inline UI (or interrupted mid-edit) may be
40
- // incomplete — the card says so instead of silently blocking Continue.
41
- const incomplete = !isKeyPersonRowValid(entry);
50
+ // incomplete — the card says so instead of silently blocking Continue. A
51
+ // missing REQUIRED email gets named specifically: "incomplete" on a row
52
+ // whose name, role and country are all filled reads as a bug.
53
+ const required = emailRequiredFor ?? new Set<KeyPersonRole>();
54
+ const incomplete = !isKeyPersonRowValid(entry, required);
55
+ const problem =
56
+ rowNeedsEmail(entry, required) && entry.email.trim() === '' && entry.name.trim().length >= 2
57
+ ? 'Email required, tap to add'
58
+ : 'Incomplete, tap to finish';
42
59
 
43
- const meta = [KEY_PERSON_ROLE_LABELS[entry.role], pct ? `${pct}% ownership` : null]
60
+ const meta = [
61
+ roleLabel ?? KEY_PERSON_ROLE_LABELS[entry.role],
62
+ entry.title.trim() || null,
63
+ pct ? `${pct}% ownership` : null,
64
+ ]
44
65
  .filter(Boolean)
45
66
  .join(' · ');
46
67
  // The flag alone doesn't say WHICH country — spell it out, alongside the
@@ -104,7 +125,7 @@ export function KeyPersonCard({
104
125
  </MyazaText>
105
126
  {incomplete ? (
106
127
  <MyazaText variant="bodySmall" color={colors.error} numberOfLines={1} style={{ marginTop: 1 }}>
107
- Incomplete — tap to finish
128
+ {problem}
108
129
  </MyazaText>
109
130
  ) : detail !== '' ? (
110
131
  <MyazaText variant="bodySmall" color={colors.textMuted} numberOfLines={1} style={{ marginTop: 1 }}>
@@ -114,6 +135,17 @@ export function KeyPersonCard({
114
135
  </View>
115
136
 
116
137
  <Icon name="pencil" size={16} color={colors.textMuted} />
138
+ {onRemove ? (
139
+ <Pressable
140
+ accessibilityRole="button"
141
+ accessibilityLabel={`Remove ${name} from this section`}
142
+ hitSlop={8}
143
+ onPress={onRemove}
144
+ style={{ marginLeft: 4, padding: 4 }}
145
+ >
146
+ <Icon name="close" size={16} color={colors.textMuted} />
147
+ </Pressable>
148
+ ) : null}
117
149
  </Pressable>
118
150
  );
119
151
  }