@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.
- package/README.md +151 -5
- package/android/build.gradle +35 -3
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +77 -0
- package/app.plugin.js +89 -8
- package/ios/HybridMyazaFaceDetector.swift +16 -3
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +23 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +9 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +43 -0
- package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +17 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +31 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +3 -0
- package/package.json +14 -2
- package/src/MyazaBiometricAuth.tsx +119 -0
- package/src/MyazaKYC.tsx +12 -0
- package/src/components/CountryField.tsx +8 -0
- package/src/components/CountryRegionPicker.tsx +184 -0
- package/src/components/DashedBorder.tsx +55 -0
- package/src/components/DialCodePicker.tsx +53 -67
- package/src/components/DialCodeRow.tsx +102 -0
- package/src/components/FramedMapPicker.tsx +122 -0
- package/src/components/GeoBadge.tsx +34 -0
- package/src/components/Icon.tsx +2 -176
- package/src/components/KycFlow.tsx +60 -32
- package/src/components/LineSkeleton.tsx +100 -0
- package/src/components/MapChrome.tsx +67 -0
- package/src/components/MapPinMarker.tsx +54 -0
- package/src/components/MapPinPicker.tsx +179 -0
- package/src/components/MilestoneTrack.tsx +155 -0
- package/src/components/MultiIdProgress.tsx +107 -0
- package/src/components/MyazaInput.tsx +39 -4
- package/src/components/MyazaSelect.tsx +2 -1
- package/src/components/OptionRow.tsx +22 -3
- package/src/components/OwnershipSlider.tsx +102 -0
- package/src/components/PhoneNumberInput.tsx +45 -4
- package/src/components/PoweredBy.tsx +5 -5
- package/src/components/PresenceBlocks.tsx +153 -0
- package/src/components/StaggerIn.tsx +63 -0
- package/src/components/StepView.tsx +43 -0
- package/src/components/StickyActions.tsx +56 -0
- package/src/components/dialCodeRows.ts +64 -0
- package/src/components/glass/FloatingSheet.tsx +73 -21
- package/src/components/glass/GlassSheet.tsx +14 -2
- package/src/components/icon-map.ts +176 -0
- package/src/components/stepHeaderMeta.tsx +45 -5
- package/src/config/addressCollection.ts +125 -0
- package/src/config/biometricOptions.ts +100 -0
- package/src/config/business.ts +26 -7
- package/src/config/businessDetailsValidity.ts +52 -0
- package/src/config/businessPrefill.ts +72 -0
- package/src/config/businessSteps.ts +20 -3
- package/src/config/consentStep.ts +19 -0
- package/src/config/keyPeople.ts +111 -7
- package/src/config/keyPeopleCorporate.ts +19 -0
- package/src/config/keyPeopleOwnerTypes.ts +12 -0
- package/src/config/keyPeopleOwners.ts +26 -0
- package/src/config/keyPeoplePrefill.ts +138 -0
- package/src/config/keyPeopleSectionDefs.ts +65 -0
- package/src/config/keyPeopleSections.ts +167 -0
- package/src/config/proofOfAddress.ts +54 -11
- package/src/config/regions.ts +24 -0
- package/src/config/registrationHint.ts +1 -1
- package/src/config/stepOrder.ts +100 -6
- package/src/config/theme.ts +8 -2
- package/src/config/uploadLimits.ts +35 -0
- package/src/config/website.ts +36 -0
- package/src/config/workflowMerge.ts +16 -5
- package/src/emrtd/activeAuth.ts +100 -0
- package/src/emrtd/extras.ts +76 -0
- package/src/emrtd/files.ts +5 -0
- package/src/emrtd/index.ts +1 -0
- package/src/emrtd/read.ts +8 -1
- package/src/emrtd/session.ts +41 -0
- package/src/emrtd/stages.ts +11 -0
- package/src/index.ts +54 -1
- package/src/lib/address-current-location.ts +171 -0
- package/src/lib/address-field-modes.ts +119 -0
- package/src/lib/address-flow.ts +172 -0
- package/src/lib/address-helpers.ts +48 -0
- package/src/lib/address-line.ts +76 -0
- package/src/lib/address-pin-move.ts +97 -0
- package/src/lib/address-step-recovery.ts +63 -0
- package/src/lib/authed-image.ts +78 -0
- package/src/lib/biometric-auth.ts +52 -0
- package/src/lib/biometric-copy.ts +51 -0
- package/src/lib/captureRing.ts +83 -0
- package/src/lib/contact-recovery.ts +44 -0
- package/src/lib/country-adoption.ts +89 -0
- package/src/lib/inferred-country.ts +65 -0
- package/src/lib/livenessLayout.ts +49 -0
- package/src/lib/map-frame.ts +159 -0
- package/src/lib/map-tiles.ts +143 -0
- package/src/lib/multi-id.ts +210 -0
- package/src/lib/poa-country-gate.ts +27 -0
- package/src/lib/resubmit.ts +130 -0
- package/src/lib/result-copy.ts +116 -0
- package/src/lib/result-wait.ts +53 -0
- package/src/lib/review-map-surface.ts +26 -0
- package/src/lib/scope.ts +31 -0
- package/src/lib/screen-corners.ts +50 -0
- package/src/lib/selfie-upload-wait.ts +79 -0
- package/src/lib/step-log.ts +24 -3
- package/src/lib/street-view-fov.ts +42 -0
- package/src/lib/use-multi-id-plan.ts +29 -0
- package/src/lib/webview-available.ts +47 -0
- package/src/liveness/useLiveness.ts +6 -1
- package/src/liveness/useModelReady.ts +65 -0
- package/src/liveness/visionCameraFaceDetector.ts +35 -0
- package/src/presence/background-math.ts +100 -0
- package/src/presence/background-store.ts +82 -0
- package/src/presence/background.ts +165 -0
- package/src/presence/foreground-service.ts +193 -0
- package/src/presence/fs.ts +51 -0
- package/src/presence/geofence.ts +41 -0
- package/src/presence/math.ts +44 -0
- package/src/presence/post.ts +40 -0
- package/src/presence/report.ts +87 -0
- package/src/presence/sampler.ts +110 -0
- package/src/presence/status.ts +92 -0
- package/src/presence/store.ts +94 -0
- package/src/presence/tier.ts +36 -0
- package/src/presence/watch-wait.ts +81 -0
- package/src/screens/AddressCountryControl.tsx +115 -0
- package/src/screens/ApplicantRoleStep.tsx +9 -5
- package/src/screens/BusinessCheckPanel.tsx +89 -0
- package/src/screens/BusinessDetailsFields.tsx +112 -0
- package/src/screens/BusinessDetailsStep.tsx +133 -145
- package/src/screens/BusinessDocumentSlot.tsx +5 -5
- package/src/screens/BusinessDocumentsStep.tsx +5 -56
- package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
- package/src/screens/BusinessPickedCard.tsx +86 -0
- package/src/screens/BusinessPickedSection.tsx +51 -0
- package/src/screens/BusinessRegistryPickers.tsx +67 -0
- package/src/screens/BusinessSandboxToggle.tsx +114 -0
- package/src/screens/BusinessSearch.tsx +199 -0
- package/src/screens/BusinessSearchResults.tsx +115 -0
- package/src/screens/CompanyInfoFields.tsx +91 -51
- package/src/screens/ConsentStep.tsx +37 -17
- package/src/screens/ContactActions.tsx +54 -0
- package/src/screens/ContactDestinationField.tsx +5 -0
- package/src/screens/ContactEntryPanel.tsx +75 -0
- package/src/screens/ContactVerificationStep.tsx +43 -42
- package/src/screens/CountrySelectStep.tsx +13 -147
- package/src/screens/IdTypeStep.tsx +12 -5
- package/src/screens/KeepLinksSheet.tsx +88 -0
- package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
- package/src/screens/KeyPeopleAwaitList.tsx +126 -0
- package/src/screens/KeyPeoplePending.tsx +154 -0
- package/src/screens/KeyPeopleSection.tsx +162 -0
- package/src/screens/KeyPeopleSectionsList.tsx +103 -0
- package/src/screens/KeyPeopleUboExemption.tsx +72 -0
- package/src/screens/KeyPersonCard.tsx +36 -4
- package/src/screens/KeyPersonForm.tsx +195 -36
- package/src/screens/KeyPersonKindToggle.tsx +68 -0
- package/src/screens/KeyPersonOwners.tsx +109 -0
- package/src/screens/KeyPersonRoleChips.tsx +68 -0
- package/src/screens/KeyPersonSheet.tsx +92 -108
- package/src/screens/LivenessAvatar.tsx +15 -3
- package/src/screens/LivenessStep.tsx +172 -52
- package/src/screens/NfcStep.tsx +14 -0
- package/src/screens/PoaDocumentTypeList.tsx +66 -0
- package/src/screens/ProofOfAddressParts.tsx +151 -0
- package/src/screens/ProofOfAddressStep.tsx +58 -96
- package/src/screens/SubmittedBadge.tsx +25 -0
- package/src/screens/SubmittedError.tsx +64 -0
- package/src/screens/SubmittedResult.tsx +117 -0
- package/src/screens/SubmittedStep.tsx +82 -119
- package/src/screens/SubmittedSuccess.tsx +127 -0
- package/src/screens/SubmittedWaiting.tsx +45 -0
- package/src/screens/address/AddressEntranceStep.tsx +161 -0
- package/src/screens/address/AddressIntroGate.tsx +148 -0
- package/src/screens/address/AddressMapStub.tsx +50 -0
- package/src/screens/address/AddressPinStep.tsx +178 -0
- package/src/screens/address/AddressReviewStep.tsx +162 -0
- package/src/screens/address/AddressSandboxTabs.tsx +158 -0
- package/src/screens/address/AddressSearchField.tsx +115 -0
- package/src/screens/address/AddressSearchStep.tsx +94 -0
- package/src/screens/address/CurrentLocationRow.tsx +135 -0
- package/src/screens/address/DetailsSheet.tsx +175 -0
- package/src/screens/address/DetailsSheetFields.tsx +184 -0
- package/src/screens/address/EntranceDropzone.tsx +178 -0
- package/src/screens/address/EntranceFraming.tsx +66 -0
- package/src/screens/address/EntrancePills.tsx +60 -0
- package/src/screens/address/FramedStreetView.tsx +155 -0
- package/src/screens/address/IntroDisclosures.tsx +210 -0
- package/src/screens/address/LabelDecisionRow.tsx +113 -0
- package/src/screens/address/PinSummaryRow.tsx +105 -0
- package/src/screens/address/ReviewAddressBand.tsx +111 -0
- package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
- package/src/screens/address/ReviewMapPicture.tsx +106 -0
- package/src/screens/address/SearchResults.tsx +105 -0
- package/src/screens/address/SearchScreen.tsx +176 -0
- package/src/screens/address/SkipForNow.tsx +43 -0
- package/src/screens/address/StreetViewChrome.tsx +81 -0
- package/src/screens/address/detail-values.ts +22 -0
- package/src/screens/address/fix-source.ts +27 -0
- package/src/screens/address/index.ts +8 -0
- package/src/screens/address/meta.ts +44 -0
- package/src/screens/address/use-address-flow.ts +200 -0
- package/src/screens/address/use-label-pin.ts +80 -0
- package/src/screens/address/use-pin-actions.ts +192 -0
- package/src/screens/biometric/BiometricAuthFlow.tsx +170 -0
- package/src/screens/consent/model.ts +79 -10
- package/src/screens/liveness/CaptureRing.tsx +91 -0
- package/src/screens/liveness/LivenessCamera.tsx +63 -0
- package/src/screens/liveness/LivenessHandover.tsx +43 -0
- package/src/screens/liveness/LivenessOutcome.tsx +4 -3
- package/src/screens/liveness/SelfiePreview.tsx +13 -4
- package/src/screens/liveness/ShutterFlash.tsx +25 -0
- package/src/screens/liveness/index.ts +4 -0
- package/src/screens/liveness/useSelfieUpload.ts +23 -3
- package/src/screens/nfc/NfcReadProgress.tsx +1 -0
- package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
- package/src/screens/useAddressPhotoAttach.ts +77 -0
- package/src/screens/useAwaitingPeople.ts +90 -0
- package/src/screens/useBusinessDocumentAttach.ts +93 -0
- package/src/screens/usePoaAttach.ts +12 -15
- package/src/services/api-biometric.ts +37 -0
- package/src/services/api-types-biometric.ts +41 -0
- package/src/services/api-types.ts +251 -3
- package/src/services/api-verify-types.ts +90 -1
- package/src/services/api.ts +237 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/errors.ts +10 -2
- package/src/services/location.ts +234 -0
- package/src/services/uploadErrors.ts +30 -0
- package/src/services/workflowGate.ts +8 -0
- package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
- package/src/store/address.ts +97 -0
- package/src/store/businessCheck.ts +113 -0
- package/src/store/derive.ts +44 -4
- package/src/store/kycStore.ts +352 -16
- package/src/store/serverConfig.ts +20 -0
- package/src/store/session.ts +227 -0
- package/src/store/state.ts +289 -2
- package/src/store/submit.ts +80 -13
- package/src/store/submitApplicant.ts +5 -11
- package/src/types/business.ts +23 -2
- package/src/types/config.ts +70 -2
- package/src/types/verification.ts +13 -1
- package/src/types/workflow.ts +112 -1
- 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 {
|
|
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 {
|
|
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 —
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
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
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
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)}
|
|
97
|
+
? `Combined ownership would be ${fmtPct(combinedTotal)}%, over by ${fmtPct(combinedTotal - 100)}%.`
|
|
86
98
|
: null;
|
|
87
99
|
|
|
88
|
-
const canSave =
|
|
100
|
+
const canSave =
|
|
101
|
+
isKeyPersonRowValid(draft, emailRequiredFor ?? new Set()) && draft.name.trim().length >= 2;
|
|
89
102
|
|
|
90
103
|
return (
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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({
|
|
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={
|
|
70
|
+
<Icon name="scan-face" size={iconSize} color={colors.primary} />
|
|
59
71
|
) : (
|
|
60
72
|
<Image
|
|
61
73
|
source={GIFS[displayed]}
|