@myazahq/kyc-sdk-react-native 2.6.0 → 3.1.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 (104) hide show
  1. package/README.md +128 -4
  2. package/android/build.gradle +30 -16
  3. package/android/consumer-rules.pro +22 -0
  4. package/android/src/main/AndroidManifest.xml +17 -6
  5. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +27 -72
  6. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +14 -0
  7. package/android/src/main/java/com/margelo/nitro/myazakyc/MlKitModelReadiness.kt +154 -0
  8. package/app.plugin.js +57 -2
  9. package/ios/HybridMyazaTextRecognizer.swift +8 -0
  10. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +21 -0
  11. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +2 -0
  12. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +8 -0
  13. package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +16 -0
  14. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +2 -0
  15. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +31 -0
  16. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +2 -0
  17. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +2 -0
  18. package/package.json +24 -14
  19. package/src/MyazaKYC.tsx +4 -11
  20. package/src/assets/fonts/Karla_400Regular.ttf +0 -0
  21. package/src/assets/fonts/Karla_500Medium.ttf +0 -0
  22. package/src/assets/fonts/Karla_600SemiBold.ttf +0 -0
  23. package/src/assets/fonts/Karla_700Bold.ttf +0 -0
  24. package/src/assets/fonts/OFL-Karla.txt +93 -0
  25. package/src/assets/fonts/OFL-SpaceGrotesk.txt +93 -0
  26. package/src/assets/fonts/README.md +12 -0
  27. package/src/assets/fonts/SpaceGrotesk_500Medium.ttf +0 -0
  28. package/src/assets/fonts/SpaceGrotesk_600SemiBold.ttf +0 -0
  29. package/src/assets/fonts/SpaceGrotesk_700Bold.ttf +0 -0
  30. package/src/capture/useAutoCapture.ts +8 -1
  31. package/src/components/DocumentCropper.tsx +30 -24
  32. package/src/components/DocumentReview.tsx +11 -1
  33. package/src/components/DocumentReviewSide.tsx +6 -2
  34. package/src/components/DocumentReviewZoom.tsx +5 -1
  35. package/src/components/GlassIconButton.tsx +2 -5
  36. package/src/components/Icon.tsx +17 -13
  37. package/src/components/KycFlow.tsx +10 -2
  38. package/src/components/KycSheet.tsx +0 -1
  39. package/src/components/MyazaButton.tsx +9 -0
  40. package/src/components/RequiredDocumentPill.tsx +114 -0
  41. package/src/components/StepHeader.tsx +23 -3
  42. package/src/components/StepView.tsx +3 -0
  43. package/src/components/documentReviewCopy.ts +39 -0
  44. package/src/components/fonts.ts +14 -17
  45. package/src/components/icons/glyphs.ts +35 -0
  46. package/src/components/icons/index.ts +3 -0
  47. package/src/components/icons/map.ts +165 -0
  48. package/src/components/icons/names.ts +86 -0
  49. package/src/components/stepHeaderMeta.tsx +21 -1
  50. package/src/config/documentCaptureMethods.ts +31 -0
  51. package/src/config/stepOrder.ts +20 -5
  52. package/src/config/supportingDocuments.ts +131 -0
  53. package/src/config/theme.ts +29 -3
  54. package/src/config/workflowMerge.ts +15 -0
  55. package/src/index.ts +5 -10
  56. package/src/lib/documentCaptureCheck.ts +136 -0
  57. package/src/lib/model-ready.ts +82 -0
  58. package/src/lib/prime-models.ts +50 -0
  59. package/src/lib/resubmit.ts +36 -7
  60. package/src/lib/selfie-sharpness.ts +211 -0
  61. package/src/lib/supportingDocumentsIntro.ts +35 -0
  62. package/src/liveness/avatarSource.ts +59 -0
  63. package/src/liveness/useModelReady.ts +9 -55
  64. package/src/mrz/extract.ts +71 -4
  65. package/src/mrz/textRecognizer.ts +36 -0
  66. package/src/mrz/useTextModelReady.ts +21 -0
  67. package/src/screens/BusinessDocumentSlot.tsx +37 -8
  68. package/src/screens/DocumentCaptureStep.tsx +177 -93
  69. package/src/screens/LivenessAvatar.tsx +18 -13
  70. package/src/screens/LivenessStep.tsx +37 -0
  71. package/src/screens/MrzScanView.tsx +33 -0
  72. package/src/screens/SupportingDocumentCard.tsx +134 -0
  73. package/src/screens/SupportingDocumentsStep.tsx +203 -0
  74. package/src/screens/consent/model.ts +22 -5
  75. package/src/screens/document/CaptureCheckNotice.tsx +77 -0
  76. package/src/screens/document/UploadPhase.tsx +177 -0
  77. package/src/screens/document/useDocumentCamera.ts +109 -0
  78. package/src/screens/liveness/CaptureRing.tsx +31 -17
  79. package/src/screens/liveness/LivenessOutcome.tsx +4 -1
  80. package/src/screens/liveness/SelfiePreview.tsx +19 -4
  81. package/src/screens/supportingDocumentParts.tsx +126 -0
  82. package/src/screens/useBusinessDocumentAttach.ts +20 -9
  83. package/src/services/api-types.ts +31 -2
  84. package/src/services/api.ts +19 -3
  85. package/src/services/deviceMetadata.ts +1 -1
  86. package/src/services/mediaCompress.ts +23 -5
  87. package/src/specs/MyazaTextRecognizer.nitro.ts +29 -0
  88. package/src/store/derive.ts +11 -0
  89. package/src/store/kycStore.ts +23 -2
  90. package/src/store/session.ts +18 -0
  91. package/src/store/state.ts +26 -0
  92. package/src/store/submit.ts +11 -0
  93. package/src/types/config.ts +25 -0
  94. package/src/types/workflow.ts +39 -0
  95. package/src/MyazaBiometricAuth.tsx +0 -119
  96. package/src/assets/liveness/Blink.gif +0 -0
  97. package/src/assets/liveness/Nod.gif +0 -0
  98. package/src/assets/liveness/Smile.gif +0 -0
  99. package/src/assets/liveness/Turn.gif +0 -0
  100. package/src/components/icon-map.ts +0 -176
  101. package/src/lib/biometric-auth.ts +0 -52
  102. package/src/screens/biometric/BiometricAuthFlow.tsx +0 -170
  103. package/src/services/api-biometric.ts +0 -37
  104. package/src/services/api-types-biometric.ts +0 -41
@@ -62,6 +62,7 @@ import { IDLE_SELFIE_UPLOAD } from '../lib/selfie-upload-wait';
62
62
  import { resetBusinessCheck, runBusinessCheck } from './businessCheck';
63
63
  import { startAttemptSession, watchSessionProgress } from './session';
64
64
  import { applicantMediaCaptured, buildApplicantVerifyRequest } from './submitApplicant';
65
+ import { keptIdType } from '../lib/resubmit';
65
66
 
66
67
  export * from './state';
67
68
  export {
@@ -102,7 +103,10 @@ export function createKycStore(
102
103
  businessCheck: { ...EMPTY_BUSINESS_CHECK },
103
104
  selectedCountry: null,
104
105
  countryAutoPicked: false,
105
- selectedIdType: null,
106
+ // A send-back that did not ask for the ID keeps the original one: the
107
+ // ID steps are narrowed out, so the kept ID is selected here instead.
108
+ // A restored session only overwrites it with a saved pick.
109
+ selectedIdType: keptIdType(config.resubmit) as KycState['selectedIdType'],
106
110
  idNumber: null,
107
111
  multiIdSlotIndex: 0,
108
112
  multiIdSlots: [],
@@ -122,6 +126,7 @@ export function createKycStore(
122
126
  contactChallenge: null,
123
127
  business: EMPTY_BUSINESS,
124
128
  businessApplication: EMPTY_BUSINESS_APPLICATION,
129
+ supportingDocuments: [],
125
130
  applicantKeyPersonId: null,
126
131
  keyPeopleInvites: [],
127
132
  captureIntegrity: null,
@@ -419,6 +424,20 @@ export function createKycStore(
419
424
  }));
420
425
  },
421
426
 
427
+ setSupportingDocument(doc) {
428
+ set((s) => ({
429
+ // One upload per document, like the business slots: re-uploading
430
+ // replaces rather than appends.
431
+ supportingDocuments: [...s.supportingDocuments.filter((d) => d.type !== doc.type), doc],
432
+ }));
433
+ },
434
+
435
+ removeSupportingDocument(type) {
436
+ set((s) => ({
437
+ supportingDocuments: s.supportingDocuments.filter((d) => d.type !== type),
438
+ }));
439
+ },
440
+
422
441
  removeBusinessDocument(type) {
423
442
  set((s) => ({
424
443
  businessApplication: {
@@ -661,7 +680,9 @@ export function createKycStore(
661
680
  currentStep: opening,
662
681
  selectedCountry: null,
663
682
  countryAutoPicked: false,
664
- selectedIdType: null,
683
+ // Same kept ID as at creation: the narrowed flow has no picker to
684
+ // choose it again.
685
+ selectedIdType: keptIdType(config.resubmit) as KycState['selectedIdType'],
665
686
  idNumber: null,
666
687
  multiIdSlotIndex: 0,
667
688
  multiIdSlots: [],
@@ -90,6 +90,17 @@ export function restoreAttemptProgress(
90
90
  const s = store.getState();
91
91
  const d = (progress.data ?? {}) as Record<string, unknown>;
92
92
 
93
+ // Validate-and-drop: a snapshot written by an older build must degrade to
94
+ // restoring less, never to breaking the flow.
95
+ const restoredDocs = Array.isArray(d['supportingDocuments'])
96
+ ? (d['supportingDocuments'] as unknown[]).filter(
97
+ (doc): doc is { type: string; mediaId: string } =>
98
+ !!doc &&
99
+ typeof doc === 'object' &&
100
+ typeof (doc as { type?: unknown }).type === 'string' &&
101
+ typeof (doc as { mediaId?: unknown }).mediaId === 'string',
102
+ )
103
+ : null;
93
104
  const app = d['businessApplication'] as
94
105
  | (Partial<KycState['businessApplication']> & { keyPeople?: Array<Record<string, unknown>> })
95
106
  | undefined;
@@ -116,6 +127,7 @@ export function restoreAttemptProgress(
116
127
  ...(d['business'] && typeof d['business'] === 'object'
117
128
  ? { business: { ...s.business, ...(d['business'] as object) } }
118
129
  : {}),
130
+ ...(restoredDocs ? { supportingDocuments: restoredDocs } : {}),
119
131
  ...(app
120
132
  ? {
121
133
  businessApplication: {
@@ -180,6 +192,12 @@ export function progressFromState(s: ReturnType<KycStore['getState']>): Record<s
180
192
  idNumber: s.idNumber || undefined,
181
193
  business: s.business,
182
194
  businessApplication: s.businessApplication,
195
+ // The uploads without their preview names: a restored attempt shows the
196
+ // slot as uploaded, which is what the mediaId is for.
197
+ supportingDocuments:
198
+ s.supportingDocuments.length > 0
199
+ ? s.supportingDocuments.map((d) => ({ type: d.type, mediaId: d.mediaId }))
200
+ : undefined,
183
201
  contact: s.contact,
184
202
  questionnaireAnswers: s.questionnaireAnswers,
185
203
  address: s.address ?? undefined,
@@ -402,6 +402,24 @@ export interface KycState {
402
402
  business: BusinessState;
403
403
  /** The rest of the KYB application: people, documents, applicant role. */
404
404
  businessApplication: BusinessApplicationState;
405
+
406
+ /**
407
+ * Supporting documents the person uploaded — artefacts the org holds on
408
+ * file, not the identity evidence this verification is decided on.
409
+ * `fileName` is display only and never rides the wire or session progress.
410
+ */
411
+ supportingDocuments: Array<{
412
+ type: string;
413
+ mediaId: string;
414
+ fileName?: string;
415
+ /**
416
+ * Local URI of the picked image, for the card's thumbnail. Display only,
417
+ * like `fileName`: it never rides the wire or session progress, so a
418
+ * restored attempt shows the slot uploaded without a preview.
419
+ */
420
+ previewUri?: string;
421
+ isPdf?: boolean;
422
+ }>;
405
423
  /**
406
424
  * Set by a KYB submission that requires applicant verification — the
407
425
  * KeyPerson id the applicant's own individual check links back to.
@@ -500,6 +518,14 @@ export interface KycState {
500
518
  setUboUnidentifiable: (checked: boolean) => void;
501
519
  setBusinessDocument: (doc: BusinessDocumentUpload) => void;
502
520
  removeBusinessDocument: (type: string) => void;
521
+ setSupportingDocument: (doc: {
522
+ type: string;
523
+ mediaId: string;
524
+ fileName?: string;
525
+ previewUri?: string;
526
+ isPdf?: boolean;
527
+ }) => void;
528
+ removeSupportingDocument: (type: string) => void;
503
529
  setApplicant: (role: ApplicantRole, name: string, keyPersonIndex?: number | null) => void;
504
530
  setCaptureIntegrity: (integrity: CaptureIntegrity) => void;
505
531
  setMrzScan: (scan: MrzScan) => void;
@@ -99,6 +99,17 @@ export function buildVerifyRequest(
99
99
  : state.mediaIds,
100
100
  ...(state.config.workflowId ? { workflowId: state.config.workflowId } : {}),
101
101
  ...(state.poaDocumentType ? { proofOfAddressType: state.poaDocumentType } : {}),
102
+ // Supporting documents — artefacts held on file. The server validates them
103
+ // against the resolved workflow's request list and drops anything it did
104
+ // not ask for. File names are display only and never ride the wire.
105
+ ...(state.supportingDocuments.length > 0
106
+ ? {
107
+ supportingDocuments: state.supportingDocuments.map((d) => ({
108
+ type: d.type,
109
+ mediaId: d.mediaId,
110
+ })),
111
+ }
112
+ : {}),
102
113
  // The smart address, when the step gathered one. Sent on individual AND
103
114
  // business submissions (a KYB flow's pin is the business premises) — the
104
115
  // server validates it against the workflow either way.
@@ -22,6 +22,7 @@ import type {
22
22
  NfcConfig,
23
23
  PhoneVerificationConfig,
24
24
  ProofOfAddressConfig,
25
+ SupportingDocumentsConfig,
25
26
  QuestionnaireConfig,
26
27
  WorkflowCountry,
27
28
  MultiIdConfig,
@@ -57,6 +58,7 @@ export type KYCStep =
57
58
  | 'applicant-role'
58
59
  | 'liveness'
59
60
  | 'proof-of-address'
61
+ | 'supporting-documents'
60
62
  // The address flow, in order: find it (search) → confirm it (the PIN step,
61
63
  // which keeps the original 'address-collection' wire name so older session
62
64
  // progress restores cleanly) → show it (entrance photo) → commit it.
@@ -142,6 +144,12 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
142
144
  * mirroring the web SDK.
143
145
  */
144
146
  businessName?: string;
147
+ /**
148
+ * The applicant's email, when you already hold it. Not asked for in the flow:
149
+ * it is submitted so your organisation can have the applicant emailed about a
150
+ * decision, for example once their verification is approved.
151
+ */
152
+ email?: string;
145
153
  };
146
154
 
147
155
  /** Enable the live-selfie capture step. */
@@ -157,6 +165,15 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
157
165
  */
158
166
  allowDocumentUpload?: boolean;
159
167
 
168
+ /**
169
+ * Allow photographing the document with the live camera. Default `true`.
170
+ * When `false`, the document step never asks for camera access: the
171
+ * applicant chooses a photo of each side from the device instead. At least
172
+ * one of this and `allowDocumentUpload` stays on, so a config that switches
173
+ * both off keeps the camera.
174
+ */
175
+ allowDocumentScan?: boolean;
176
+
160
177
  /** Enable liveness detection during selfie capture. Default `true`. */
161
178
  enableLiveness?: boolean;
162
179
 
@@ -231,6 +248,14 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
231
248
  /** Proof-of-address document check, after capture. */
232
249
  proofOfAddress?: ProofOfAddressConfig;
233
250
 
251
+ /**
252
+ * Supporting documents: artefacts the org holds ON FILE, which are NOT the
253
+ * identity evidence the verification is decided on. The org names each one
254
+ * and writes the guidance under it; the result of any check never changes
255
+ * the verification's own status.
256
+ */
257
+ supportingDocuments?: SupportingDocumentsConfig;
258
+
234
259
  /** Address Intelligence: a map-pin smart address (+ optional door photo and
235
260
  * directions), corroborated server-side. KYC AND KYB (premises pin). */
236
261
  addressCollection?: AddressCollectionConfig;
@@ -24,6 +24,45 @@ export type PoaDocumentType =
24
24
  * server; read here only to word the step. */
25
25
  export type PoaNameRule = 'required' | 'optional' | 'off';
26
26
 
27
+ /**
28
+ * One requested supporting document. There is no catalogue: the organisation
29
+ * names its own, so the SDK renders the title and guidance the workflow sent.
30
+ */
31
+ export interface SupportingDocumentRequest {
32
+ /** The organisation's own slug — the wire `type` this upload submits as. */
33
+ key: string;
34
+ /** The title the applicant reads. A document without one is not asked for. */
35
+ label?: string;
36
+ /** Guidance under the slot: which document, and what it has to show. */
37
+ description?: string;
38
+ /** Server-side only: what the document is corroborated against. */
39
+ checks?: Array<'id_number' | 'name'>;
40
+ /**
41
+ * The named values the server will read off it. The SDK reads the NAMES
42
+ * alone, to tell the applicant what the document is being taken for; the
43
+ * reading itself is entirely server-side.
44
+ */
45
+ fields?: Array<{ key: string; label?: string }>;
46
+ required?: boolean;
47
+ /** Which verified IDs it is asked for, as "CC/idType". Absent = every ID. */
48
+ idTypes?: string[];
49
+ /**
50
+ * Offer it on every flow, whatever ID was verified, so `idTypes` decides only
51
+ * who MUST provide it rather than who sees the slot. Absent = the scope hides
52
+ * it from everybody else, which is the default.
53
+ */
54
+ alwaysAsk?: boolean;
55
+ }
56
+
57
+ export interface SupportingDocumentsConfig {
58
+ enabled?: boolean;
59
+ types?: SupportingDocumentRequest[];
60
+ /** Server-side only. */
61
+ retentionDays?: number;
62
+ /** Server-side only. */
63
+ returnedData?: string[];
64
+ }
65
+
27
66
  export interface ProofOfAddressConfig {
28
67
  /** Adds the Proof of Address step (after capture, before the questionnaire). */
29
68
  enabled?: boolean;
@@ -1,119 +0,0 @@
1
- import React, { useCallback, useState } from 'react';
2
- import { Modal, Platform } from 'react-native';
3
-
4
- import { KycRuntimeProvider, MyazaThemeProvider } from './components/runtime';
5
- import { MyazaButton } from './components/MyazaButton';
6
- import { BiometricAuthFlow } from './screens/biometric/BiometricAuthFlow';
7
- import { primeFaceModel } from './liveness/visionCameraFaceDetector';
8
- import { defaultReauthLabel } from './lib/biometric-auth';
9
- import type { KYCAppearance, ResolvedKYCConfig, SupportedCountry } from './types/config';
10
- import type { KYCError } from './types/verification';
11
- import type { BiometricAuthResponse } from './services/api-types-biometric';
12
-
13
- // ---------------------------------------------------------------------------
14
- // MyazaBiometricAuth — returning-user face re-authentication ("prove it's
15
- // still you"). Mirrors the web SDK's MyazaBiometricAuth: a verified user
16
- // re-authenticates with a live selfie matched 1:1 against their enrollment
17
- // reference (no gov-DB call, no re-KYC). Self-contained: its own trigger +
18
- // modal, hosting the SDK's real liveness step inside a scoped runtime.
19
- // ---------------------------------------------------------------------------
20
-
21
- export interface MyazaBiometricAuthProps {
22
- /** Publishable API key (`pk_…`). The environment is derived from its prefix. */
23
- apiKey: string;
24
- /** Dev-only base-URL override for `pk_dev_` keys. */
25
- devUrl?: string;
26
- /** The org's user reference (Entity.externalUserId) to re-authenticate. */
27
- externalUserId: string;
28
- /** Presence Intelligence method (default 'gestures'). */
29
- livenessMode?: 'gestures' | 'flash' | 'both';
30
- flashSequenceLength?: number;
31
- appearance?: KYCAppearance;
32
- disableClose?: boolean;
33
- /** Open the modal on mount (no trigger button is rendered). */
34
- defaultOpen?: boolean;
35
- /** Custom trigger label (default "Verify it's you"). */
36
- children?: string;
37
- disabled?: boolean;
38
- onOpen?: () => void;
39
- /** The user re-authenticated. `token` is a single-use proof — verify it from
40
- * your backend with a secret key at `/biometric/verify-proof`. */
41
- onAuthenticated?: (result: { attemptId: string; confidence: number | null; token?: string }) => void;
42
- /** The check ran but the user did not pass (no match / liveness failed). */
43
- onFailed?: (result: { status: BiometricAuthResponse['status']; attemptId: string; confidence: number | null }) => void;
44
- /** A technical error (network, not enrolled, insufficient credits, …). */
45
- onError?: (error: KYCError) => void;
46
- onClose?: () => void;
47
- }
48
-
49
- const MODAL_PRESENTATION = Platform.OS === 'ios' ? 'pageSheet' : 'fullScreen';
50
-
51
- export function MyazaBiometricAuth(props: MyazaBiometricAuthProps): React.ReactElement {
52
- const { children, disabled, defaultOpen, onOpen, onAuthenticated, onFailed, onError, onClose } = props;
53
- const [open, setOpen] = useState(defaultOpen === true);
54
-
55
- // The liveness step reads exactly the keys a KYC flow's config carries; the
56
- // scope parks the step order on consent → liveness → submitted, and the
57
- // flow only ever mounts the middle one. `country` is required by the
58
- // resolved shape but nothing on this path reads it.
59
- const config: ResolvedKYCConfig = {
60
- apiKey: props.apiKey,
61
- devUrl: props.devUrl,
62
- country: 'NG' as SupportedCountry,
63
- scope: 'biometric-authentication',
64
- enableSelfie: true,
65
- livenessMode: props.livenessMode ?? 'gestures',
66
- flashSequenceLength: props.flashSequenceLength,
67
- appearance: props.appearance,
68
- disableClose: props.disableClose,
69
- userId: props.externalUserId,
70
- onError,
71
- };
72
-
73
- const openFlow = useCallback(() => {
74
- void primeFaceModel();
75
- setOpen(true);
76
- onOpen?.();
77
- }, [onOpen]);
78
- const close = useCallback(() => {
79
- setOpen(false);
80
- onClose?.();
81
- }, [onClose]);
82
- const blockDismiss = props.disableClose === true;
83
-
84
- return (
85
- <MyazaThemeProvider appearance={props.appearance}>
86
- {defaultOpen !== true && (
87
- <MyazaButton
88
- label={children ?? defaultReauthLabel(props.appearance?.companyName)}
89
- disabled={disabled}
90
- fullWidth={false}
91
- onPress={openFlow}
92
- />
93
- )}
94
- <Modal
95
- visible={open}
96
- animationType="slide"
97
- presentationStyle={blockDismiss ? 'fullScreen' : MODAL_PRESENTATION}
98
- statusBarTranslucent
99
- navigationBarTranslucent
100
- onRequestClose={blockDismiss ? () => undefined : close}
101
- onDismiss={blockDismiss ? undefined : close}
102
- >
103
- {open ? (
104
- // A fresh store per open: the runtime provider creates it once per
105
- // mount, and the modal's children mount once per open.
106
- <KycRuntimeProvider config={config}>
107
- <BiometricAuthFlow
108
- externalUserId={props.externalUserId}
109
- onAuthenticated={(r) => onAuthenticated?.({ attemptId: r.attemptId, confidence: r.confidence, token: r.token })}
110
- onFailed={(r) => onFailed?.({ status: r.status, attemptId: r.attemptId, confidence: r.confidence })}
111
- onError={onError}
112
- onClose={close}
113
- />
114
- </KycRuntimeProvider>
115
- ) : null}
116
- </Modal>
117
- </MyazaThemeProvider>
118
- );
119
- }
Binary file
Binary file
Binary file
Binary file
@@ -1,176 +0,0 @@
1
- import {
2
- Info, BadgeCheck, BookUser, Building2, Stamp, Calendar, Camera, CameraOff, ChevronDown,
3
- ChevronLeft, ChevronRight, Car, Check, CircleAlert, Contact, CreditCard, Fingerprint,
4
- FileText, FlaskConical, IdCard, Image as ImageIcon, Landmark, Lightbulb, LocateFixed, MapPin,
5
- MapPinHouse, MapPinCheck, MapPinned, Radar, BellRing, SlidersHorizontal, Minus, Globe, House,
6
- Mail, Maximize2, Lock, MessageCircle, MessageSquare, Moon, MoveLeft, Nfc, Filter, Pencil,
7
- PencilLine, Smartphone, RefreshCw, RefreshCcw, ScanFace, ScanLine, Search, Zap, ZapOff,
8
- CircleDashed, CircleCheck, CircleHelp, CircleX, ShieldCheck, Sun, Timer, Upload, Plus,
9
- UserRound, UserRoundPlus, Link2, Copy, Share2, UsersRound, VideoOff, Vote, X,
10
- type LucideIcon
11
- } from 'lucide-react-native';
12
-
13
- // Centralised icon set — the SAME Lucide icons the Flutter SDK uses
14
- // (`lucide_icons_flutter`) and the web SDK uses (`lucide-react`), so the
15
- // glyphs are pixel-identical across all three platforms. The component that
16
- // draws them is Icon.tsx (200-line split).
17
-
18
- export type IconName =
19
- // chrome
20
- | 'close'
21
- | 'back'
22
- | 'moon'
23
- | 'sun'
24
- | 'check'
25
- | 'lock'
26
- | 'alert'
27
- | 'flask'
28
- | 'refresh'
29
- | 'refresh-ccw'
30
- | 'maximize'
31
- | 'calendar'
32
- | 'image'
33
- | 'chevron-left'
34
- | 'chevron-right'
35
- | 'chevron-down'
36
- // consent hero + process steps
37
- | 'shield'
38
- | 'badge-check'
39
- | 'circle-dashed'
40
- | 'circle-check'
41
- | 'circle-help'
42
- | 'circle-x'
43
- | 'building-2'
44
- | 'stamp'
45
- | 'user'
46
- | 'user-plus'
47
- | 'plus'
48
- | 'info'
49
- | 'users'
50
- | 'scan-line'
51
- | 'scan-face'
52
- | 'nfc'
53
- | 'filter'
54
- | 'pencil'
55
- | 'pencil-line'
56
- | 'search'
57
- | 'zap'
58
- | 'link'
59
- | 'copy'
60
- | 'share'
61
- | 'zap-off'
62
- | 'timer'
63
- // id-type glyphs
64
- | 'fingerprint'
65
- | 'credit-card'
66
- | 'globe'
67
- | 'car'
68
- | 'vote'
69
- | 'file-text'
70
- | 'house'
71
- | 'landmark'
72
- | 'id-card'
73
- | 'contact'
74
- | 'passport'
75
- | 'camera'
76
- | 'camera-off'
77
- | 'video-off'
78
- | 'upload'
79
- | 'x'
80
- // address collection (map picker + the presence milestone track)
81
- | 'map-pin'
82
- | 'map-pin-house'
83
- | 'map-pin-check'
84
- | 'map-pinned'
85
- | 'radar'
86
- | 'bell-ring'
87
- | 'sliders'
88
- | 'locate'
89
- | 'minus'
90
- | 'lightbulb'
91
- // contact verification (OTP channels + step footer)
92
- | 'mail'
93
- | 'smartphone'
94
- | 'message-square'
95
- | 'message-circle';
96
-
97
- // Maps each name to its Lucide component. Names mirror the Flutter SDK exactly:
98
- // shieldCheck · badgeCheck · userRound · scanLine · scanFace · fingerprint ·
99
- // creditCard · globe · car · vote · fileText (+ chrome: X / ArrowLeft /
100
- // Moon / Sun / Check / Lock / CircleAlert / RefreshCw / Camera).
101
- export const ICONS: Record<IconName, LucideIcon> = {
102
- close: X,
103
- back: MoveLeft, // longer-shaft left arrow, matching Flutter's keyboard_backspace
104
- moon: Moon,
105
- sun: Sun,
106
- check: Check,
107
- lock: Lock,
108
- alert: CircleAlert,
109
- // The environment banner's mark, shared with the web SDK (same lucide glyph)
110
- // and mirrored by Flutter's Icons.science_outlined.
111
- flask: FlaskConical,
112
- refresh: RefreshCw,
113
- 'refresh-ccw': RefreshCcw,
114
- maximize: Maximize2,
115
- calendar: Calendar,
116
- image: ImageIcon,
117
- 'chevron-left': ChevronLeft,
118
- 'chevron-right': ChevronRight,
119
- 'chevron-down': ChevronDown,
120
- shield: ShieldCheck,
121
- 'badge-check': BadgeCheck,
122
- 'circle-dashed': CircleDashed,
123
- 'circle-check': CircleCheck,
124
- 'circle-help': CircleHelp,
125
- 'circle-x': CircleX,
126
- 'building-2': Building2,
127
- stamp: Stamp,
128
- user: UserRound,
129
- info: Info,
130
- 'user-plus': UserRoundPlus,
131
- plus: Plus,
132
- link: Link2,
133
- copy: Copy,
134
- share: Share2,
135
- users: UsersRound,
136
- 'scan-line': ScanLine,
137
- 'scan-face': ScanFace,
138
- nfc: Nfc,
139
- filter: Filter,
140
- pencil: Pencil,
141
- 'pencil-line': PencilLine,
142
- search: Search,
143
- zap: Zap,
144
- 'zap-off': ZapOff,
145
- timer: Timer,
146
- fingerprint: Fingerprint,
147
- 'credit-card': CreditCard,
148
- globe: Globe,
149
- car: Car,
150
- vote: Vote,
151
- 'file-text': FileText,
152
- house: House,
153
- landmark: Landmark,
154
- 'map-pin': MapPin,
155
- 'map-pin-house': MapPinHouse,
156
- 'map-pin-check': MapPinCheck,
157
- 'map-pinned': MapPinned,
158
- radar: Radar,
159
- 'bell-ring': BellRing,
160
- sliders: SlidersHorizontal,
161
- locate: LocateFixed,
162
- minus: Minus,
163
- 'id-card': IdCard,
164
- contact: Contact,
165
- passport: BookUser,
166
- camera: Camera,
167
- 'camera-off': CameraOff,
168
- 'video-off': VideoOff,
169
- upload: Upload,
170
- x: X,
171
- lightbulb: Lightbulb,
172
- mail: Mail,
173
- smartphone: Smartphone,
174
- 'message-square': MessageSquare,
175
- 'message-circle': MessageCircle,
176
- };
@@ -1,52 +0,0 @@
1
- import { KYCApiError } from '../services/api';
2
- import { KYCError } from '../types/verification';
3
- import type { BiometricAuthResponse } from '../services/api-types-biometric';
4
-
5
- // ---------------------------------------------------------------------------
6
- // Biometric re-authentication — the pure half. Mirrors the web SDK's
7
- // MyazaBiometricAuth error mapping and outcome shapes (keep in lockstep).
8
- // ---------------------------------------------------------------------------
9
-
10
- export type BiometricAuthOutcome =
11
- | { kind: 'success'; result: BiometricAuthResponse }
12
- | { kind: 'failed'; result: BiometricAuthResponse }
13
- | { kind: 'error'; message: string };
14
-
15
- export interface BiometricAuthenticated {
16
- attemptId: string;
17
- confidence: number | null;
18
- /** Single-use proof; verify from your backend at `/biometric/verify-proof`. */
19
- token?: string;
20
- }
21
-
22
- export interface BiometricAuthFailed {
23
- status: BiometricAuthResponse['status'];
24
- attemptId: string;
25
- confidence: number | null;
26
- }
27
-
28
- /** A technical failure, in the SDK's typed vocabulary + a message a person
29
- * can read. Never the server's own wording — that is an internal contract. */
30
- export function mapAuthError(err: unknown): KYCError {
31
- if (err instanceof KYCApiError) {
32
- if (err.statusCode === 404 && err.code === 'not_enrolled') {
33
- return new KYCError('unknown', "You're not set up for face verification yet.");
34
- }
35
- if (err.statusCode === 402) {
36
- return new KYCError('insufficient_credits', 'Face verification is temporarily unavailable.');
37
- }
38
- if (err.statusCode === 401 || err.statusCode === 403) {
39
- return new KYCError('invalid_api_key', 'This app is not authorised for face verification.');
40
- }
41
- }
42
- return new KYCError('network_error', 'Something went wrong. Please try again.');
43
- }
44
-
45
- /** The verdict as the callbacks and the result screen read it. */
46
- export function outcomeOf(result: BiometricAuthResponse): BiometricAuthOutcome {
47
- return result.authenticated ? { kind: 'success', result } : { kind: 'failed', result };
48
- }
49
-
50
- export function defaultReauthLabel(companyName?: string): string {
51
- return companyName ? `Verify it's you with ${companyName}` : "Verify it's you";
52
- }