@myazahq/kyc-sdk-react-native 2.0.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 (87) hide show
  1. package/KycSdkReactNative.podspec +32 -0
  2. package/LICENSE +21 -0
  3. package/README.md +164 -0
  4. package/android/CMakeLists.txt +29 -0
  5. package/android/build.gradle +110 -0
  6. package/android/src/main/AndroidManifest.xml +12 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +12 -0
  8. package/android/src/main/java/co/myazahq/kyc/rn/MyazaFaceDetectorPackage.kt +36 -0
  9. package/android/src/main/java/co/myazahq/kyc/rn/MyazaStatusBarModule.kt +64 -0
  10. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +134 -0
  11. package/app.plugin.js +55 -0
  12. package/expo-module.config.json +6 -0
  13. package/ios/HybridMyazaFaceDetector.swift +233 -0
  14. package/package.json +84 -0
  15. package/react-native.config.js +23 -0
  16. package/src/MyazaKYC.tsx +235 -0
  17. package/src/__tests__/cardCrop.test.ts +39 -0
  18. package/src/__tests__/deviceMetadata.test.ts +34 -0
  19. package/src/__tests__/errors.test.ts +34 -0
  20. package/src/__tests__/flow.test.ts +61 -0
  21. package/src/__tests__/gestureDetector.test.ts +37 -0
  22. package/src/__tests__/liveness.test.ts +112 -0
  23. package/src/__tests__/resolveUrl.test.ts +64 -0
  24. package/src/__tests__/validators.test.ts +38 -0
  25. package/src/assets/liveness/Blink.gif +0 -0
  26. package/src/assets/liveness/Nod.gif +0 -0
  27. package/src/assets/liveness/Smile.gif +0 -0
  28. package/src/assets/liveness/Turn.gif +0 -0
  29. package/src/components/CameraPermissionView.tsx +116 -0
  30. package/src/components/CameraViewfinder.tsx +156 -0
  31. package/src/components/CountryFlag.tsx +52 -0
  32. package/src/components/DocumentCropper.tsx +325 -0
  33. package/src/components/GlassIconButton.tsx +92 -0
  34. package/src/components/Icon.tsx +125 -0
  35. package/src/components/KycFlow.tsx +205 -0
  36. package/src/components/KycSheet.tsx +224 -0
  37. package/src/components/MyazaAlert.tsx +57 -0
  38. package/src/components/MyazaButton.tsx +101 -0
  39. package/src/components/MyazaCard.tsx +48 -0
  40. package/src/components/MyazaInput.tsx +112 -0
  41. package/src/components/MyazaPulseLoader.tsx +71 -0
  42. package/src/components/StatusBarController.tsx +42 -0
  43. package/src/components/StepHeader.tsx +56 -0
  44. package/src/components/StepIndicator.tsx +74 -0
  45. package/src/components/Typography.tsx +69 -0
  46. package/src/components/fonts.ts +49 -0
  47. package/src/components/glass/GlassGroup.tsx +34 -0
  48. package/src/components/glass/GlassSurface.tsx +64 -0
  49. package/src/components/runtime.tsx +93 -0
  50. package/src/components/toast.tsx +154 -0
  51. package/src/components/useBranding.ts +27 -0
  52. package/src/components/useVideoRecorder.ts +122 -0
  53. package/src/config/captureSettings.ts +67 -0
  54. package/src/config/idTypes.ts +79 -0
  55. package/src/config/theme.ts +186 -0
  56. package/src/index.ts +54 -0
  57. package/src/liveness/challengeManager.ts +130 -0
  58. package/src/liveness/faceDetector.ts +79 -0
  59. package/src/liveness/gestureDetector.ts +80 -0
  60. package/src/liveness/speech.ts +66 -0
  61. package/src/liveness/types.ts +99 -0
  62. package/src/liveness/useLiveness.ts +484 -0
  63. package/src/liveness/visionCameraFaceDetector.ts +118 -0
  64. package/src/screens/ConsentStep.tsx +164 -0
  65. package/src/screens/DocumentCaptureStep.tsx +500 -0
  66. package/src/screens/IdInputStep.tsx +79 -0
  67. package/src/screens/IdTypeStep.tsx +142 -0
  68. package/src/screens/LivenessAvatar.tsx +69 -0
  69. package/src/screens/LivenessStep.tsx +615 -0
  70. package/src/screens/SubmittedStep.tsx +177 -0
  71. package/src/services/api.ts +291 -0
  72. package/src/services/cardCrop.ts +52 -0
  73. package/src/services/deviceMetadata.ts +185 -0
  74. package/src/services/errors.ts +92 -0
  75. package/src/services/mediaCompress.ts +129 -0
  76. package/src/services/resolveUrl.ts +98 -0
  77. package/src/services/retry.ts +70 -0
  78. package/src/services/validators.ts +103 -0
  79. package/src/specs/MyazaFaceDetector.nitro.ts +44 -0
  80. package/src/store/kycStore.ts +288 -0
  81. package/src/store/serverConfig.ts +78 -0
  82. package/src/types/config.ts +239 -0
  83. package/src/types/country-flag-icons.d.ts +6 -0
  84. package/src/types/verification.ts +79 -0
  85. package/src/utils/platform.ts +23 -0
  86. package/src/utils/tokens.ts +10 -0
  87. package/src/utils/uuid.ts +31 -0
@@ -0,0 +1,44 @@
1
+ import type { HybridObject } from 'react-native-nitro-modules';
2
+ import type { Frame } from 'react-native-vision-camera';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Nitro spec for the on-device face detector (VisionCamera v5).
6
+ //
7
+ // v5 replaced the old `FrameProcessorPlugin` + `VISION_EXPORT_SWIFT_FRAME_PROCESSOR`
8
+ // macro / `FrameProcessorPluginRegistry` model with **Nitro HybridObjects**. This
9
+ // `.nitro.ts` spec is the single source of truth: nitrogen generates the Swift
10
+ // (`HybridMyazaFaceDetectorSpec`) and Kotlin (`HybridMyazaFaceDetectorSpec`) base
11
+ // classes from it, and `nitro.json` autolinks the concrete impls
12
+ // (`HybridMyazaFaceDetector`) — iOS = Apple Vision, Android = Google ML Kit.
13
+ //
14
+ // The detector runs inside the camera-thread worklet (`useFrameOutput`): the
15
+ // boxed HybridObject is unboxed in the worklet and `detectFace(frame)` is called
16
+ // synchronously per frame (the frame never crosses the JS bridge).
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /**
20
+ * Per-frame face signals, identical in shape to {@link LivenessFaceData} in
21
+ * `../liveness/types`. `faceCount === 0` means "no face" (the worklet maps that
22
+ * to `onNoFace()` — Nitro return types are non-nullable, so we use the count as
23
+ * the presence signal rather than returning `null`).
24
+ */
25
+ export interface FaceResult {
26
+ headEulerAngleX: number; // pitch (nod), degrees; negative = down
27
+ headEulerAngleY: number; // yaw (turn), degrees; positive = left
28
+ headEulerAngleZ: number; // roll, degrees
29
+ smilingProbability: number; // 0–1
30
+ leftEyeOpenProbability: number; // 0–1
31
+ rightEyeOpenProbability: number; // 0–1
32
+ faceSizeRatio: number; // face width / frame width (0–1)
33
+ faceCount: number; // # faces in frame; 0 = no face
34
+ brightness: number; // mean luma of the frame (0–255), for the low-light gate
35
+ }
36
+
37
+ export interface MyazaFaceDetector extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> {
38
+ /**
39
+ * Detect the dominant face in a camera {@link Frame} and return its gesture
40
+ * signals. Called per-frame from the VisionCamera worklet. Returns
41
+ * `faceCount: 0` when no face is present.
42
+ */
43
+ detectFace(frame: Frame): FaceResult;
44
+ }
@@ -0,0 +1,288 @@
1
+ // ---------------------------------------------------------------------------
2
+ // kycStore — the main flow/state store (mirrors Flutter's KYCNotifier + KYCState).
3
+ //
4
+ // One store is created per modal instance via `createKycStore(config)` so the
5
+ // flow state (and the resolved API client) is scoped to a single launch. The
6
+ // React layer (Step 2) exposes it through context + a `useStore` selector hook.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import { createStore, type StoreApi } from 'zustand/vanilla';
10
+
11
+ import { createKYCApi, type KYCApi, type VerifyRequest } from '../services/api';
12
+ import { resolveBaseUrl, normalizeDevAssetUrl } from '../services/resolveUrl';
13
+ import { withRetry } from '../services/retry';
14
+ import { collectDeviceMetadata } from '../services/deviceMetadata';
15
+ import { requiresDocumentCapture } from '../config/idTypes';
16
+ import { generateRequestId } from '../utils/uuid';
17
+ import type { IdType, KYCStep, MyazaKYCConfig } from '../types/config';
18
+ import {
19
+ INITIAL_SERVER_CONFIG,
20
+ describeConfigError,
21
+ featuresFor,
22
+ type ServerConfigState,
23
+ } from './serverConfig';
24
+
25
+ export interface KYCMediaIds {
26
+ documentFront?: string;
27
+ documentBack?: string;
28
+ selfie?: string;
29
+ documentFrontVideo?: string;
30
+ documentBackVideo?: string;
31
+ livenessVideo?: string;
32
+ }
33
+
34
+ export interface KYCSubmissionResult {
35
+ verificationId: string;
36
+ status: 'pending';
37
+ }
38
+
39
+ export type DocumentScanPhase = 'front' | 'back' | 'complete';
40
+
41
+ /** Document-capture sub-phase — drives the sheet header title/description. */
42
+ export type DocumentCapturePhase = 'front' | 'front-preview' | 'back' | 'review';
43
+
44
+ /** The mediaIds keys settable via `setMediaId`. */
45
+ export type MediaIdKey = keyof KYCMediaIds;
46
+
47
+ export interface KycState {
48
+ config: MyazaKYCConfig;
49
+ api: KYCApi;
50
+
51
+ currentStep: KYCStep;
52
+ selectedIdType: IdType | null;
53
+ idNumber: string | null;
54
+ mediaIds: KYCMediaIds;
55
+ submissionResult: KYCSubmissionResult | null;
56
+ serverConfig: ServerConfigState;
57
+ documentScanPhase: DocumentScanPhase;
58
+ /** Sub-phase of the document-capture step — synced by the screen so the header
59
+ * title/description can be phase-aware (mirrors Flutter's docReviewPhase). */
60
+ documentCapturePhase: DocumentCapturePhase;
61
+ isLoading: boolean;
62
+ error: string | null;
63
+
64
+ // Actions
65
+ loadServerConfig: () => Promise<void>;
66
+ setIdType: (idType: IdType) => void;
67
+ setIdNumber: (idNumber: string) => void;
68
+ setMediaId: (key: MediaIdKey, mediaId: string) => void;
69
+ setDocumentMediaId: (mediaId: string, side: 'front' | 'back') => void;
70
+ setDocumentCapturePhase: (phase: DocumentCapturePhase) => void;
71
+ nextStep: () => void;
72
+ previousStep: () => void;
73
+ goToStep: (step: KYCStep) => void;
74
+ submitAsync: (onRetry?: (attempt: number, total: number) => void) => Promise<KYCSubmissionResult>;
75
+ reset: () => void;
76
+ }
77
+
78
+ export type KycStore = StoreApi<KycState>;
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Flow navigation — single source of truth for the 5-step sequence.
82
+ // ---------------------------------------------------------------------------
83
+
84
+ function livenessEnabled(state: KycState): boolean {
85
+ // Consumer baseline: liveness is on unless explicitly disabled.
86
+ if (state.config.enableLiveness === false) return false;
87
+ const idType = state.selectedIdType;
88
+ if (!idType) return true;
89
+ // Server flag wins when present; otherwise keep the consumer baseline (on).
90
+ const features = featuresFor(state.serverConfig, state.config.country, idType);
91
+ return features ? features.livenessCheck : true;
92
+ }
93
+
94
+ /** The step that follows `step`, given the current selection + flags. */
95
+ function nextStepAfter(step: KYCStep, state: KycState): KYCStep {
96
+ switch (step) {
97
+ case 'consent':
98
+ return 'id-type';
99
+ case 'id-type':
100
+ return state.selectedIdType && requiresDocumentCapture(state.selectedIdType)
101
+ ? 'document-capture'
102
+ : 'id-input';
103
+ case 'document-capture':
104
+ case 'id-input':
105
+ return livenessEnabled(state) ? 'liveness' : 'submitted';
106
+ case 'liveness':
107
+ return 'submitted';
108
+ case 'submitted':
109
+ return 'submitted';
110
+ default:
111
+ return step;
112
+ }
113
+ }
114
+
115
+ /** The step before `step` (for the back button). */
116
+ function previousStepBefore(step: KYCStep, state: KycState): KYCStep {
117
+ switch (step) {
118
+ case 'id-type':
119
+ return 'consent';
120
+ case 'document-capture':
121
+ case 'id-input':
122
+ return 'id-type';
123
+ case 'liveness':
124
+ return state.selectedIdType && requiresDocumentCapture(state.selectedIdType)
125
+ ? 'document-capture'
126
+ : 'id-input';
127
+ case 'submitted':
128
+ // Submitted is terminal; back is a no-op in practice.
129
+ return livenessEnabled(state) ? 'liveness' : step;
130
+ case 'consent':
131
+ default:
132
+ return 'consent';
133
+ }
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Store factory
138
+ // ---------------------------------------------------------------------------
139
+
140
+ export function createKycStore(config: MyazaKYCConfig): KycStore {
141
+ const baseUrl = resolveBaseUrl(config.apiKey, config.devUrl);
142
+ const api = createKYCApi(baseUrl, config.apiKey);
143
+
144
+ return createStore<KycState>((set, get) => {
145
+ function emitStepChange(step: KYCStep): void {
146
+ config.onStepChange?.(step);
147
+ }
148
+
149
+ return {
150
+ config,
151
+ api,
152
+
153
+ currentStep: 'consent',
154
+ selectedIdType: null,
155
+ idNumber: null,
156
+ mediaIds: {},
157
+ submissionResult: null,
158
+ serverConfig: INITIAL_SERVER_CONFIG,
159
+ documentScanPhase: 'front',
160
+ documentCapturePhase: 'front',
161
+ isLoading: false,
162
+ error: null,
163
+
164
+ async loadServerConfig() {
165
+ set((s) => ({ serverConfig: { ...s.serverConfig, status: 'loading' } }));
166
+ try {
167
+ const res = await api.config();
168
+ // Make a local dev server's hardcoded `localhost` logo URL reachable on
169
+ // the Android emulator (rewrites the origin to the SDK base; no-op for
170
+ // production CDN URLs).
171
+ const branding = res.branding
172
+ ? { ...res.branding, logo: normalizeDevAssetUrl(res.branding.logo, baseUrl) }
173
+ : res.branding;
174
+ set({
175
+ serverConfig: {
176
+ status: 'ready',
177
+ idTypes: res.idTypes,
178
+ branding,
179
+ environment: res.environment,
180
+ fatal: false,
181
+ },
182
+ });
183
+ } catch (err) {
184
+ const described = describeConfigError(err);
185
+ set((s) => ({
186
+ serverConfig: { ...s.serverConfig, status: 'error', ...described },
187
+ }));
188
+ }
189
+ },
190
+
191
+ setIdType(idType) {
192
+ set({ selectedIdType: idType });
193
+ },
194
+
195
+ setIdNumber(idNumber) {
196
+ set({ idNumber });
197
+ },
198
+
199
+ setMediaId(key, mediaId) {
200
+ set((s) => ({ mediaIds: { ...s.mediaIds, [key]: mediaId } }));
201
+ },
202
+
203
+ setDocumentMediaId(mediaId, side) {
204
+ set((s) => ({
205
+ mediaIds: {
206
+ ...s.mediaIds,
207
+ [side === 'front' ? 'documentFront' : 'documentBack']: mediaId,
208
+ },
209
+ documentScanPhase: side === 'front' ? 'back' : 'complete',
210
+ }));
211
+ },
212
+
213
+ setDocumentCapturePhase(phase) {
214
+ if (get().documentCapturePhase !== phase) set({ documentCapturePhase: phase });
215
+ },
216
+
217
+ nextStep() {
218
+ const next = nextStepAfter(get().currentStep, get());
219
+ if (next !== get().currentStep) {
220
+ set({ currentStep: next });
221
+ emitStepChange(next);
222
+ }
223
+ },
224
+
225
+ previousStep() {
226
+ const prev = previousStepBefore(get().currentStep, get());
227
+ if (prev !== get().currentStep) {
228
+ set({ currentStep: prev });
229
+ emitStepChange(prev);
230
+ }
231
+ },
232
+
233
+ goToStep(step) {
234
+ if (step !== get().currentStep) {
235
+ set({ currentStep: step });
236
+ emitStepChange(step);
237
+ }
238
+ },
239
+
240
+ async submitAsync(onRetry) {
241
+ set({ isLoading: true, error: null });
242
+ const state = get();
243
+ const request: VerifyRequest = {
244
+ country: state.config.country,
245
+ idType: state.selectedIdType ?? '',
246
+ idNumber: state.idNumber ?? undefined,
247
+ userData: state.config.userData,
248
+ mediaIds: state.mediaIds,
249
+ metadata: {
250
+ requestId: generateRequestId(),
251
+ device: collectDeviceMetadata() as unknown as Record<string, unknown>,
252
+ ...(state.config.metadata ?? {}),
253
+ },
254
+ };
255
+ try {
256
+ const res = await withRetry(() => api.verify(request), { onRetry });
257
+ const result: KYCSubmissionResult = {
258
+ verificationId: res.verificationId,
259
+ status: 'pending',
260
+ };
261
+ set({ submissionResult: result, isLoading: false });
262
+ return result;
263
+ } catch (err) {
264
+ set({ isLoading: false, error: err instanceof Error ? err.message : 'Submission failed' });
265
+ throw err;
266
+ }
267
+ },
268
+
269
+ reset() {
270
+ set({
271
+ currentStep: 'consent',
272
+ selectedIdType: null,
273
+ idNumber: null,
274
+ mediaIds: {},
275
+ submissionResult: null,
276
+ documentScanPhase: 'front',
277
+ documentCapturePhase: 'front',
278
+ isLoading: false,
279
+ error: null,
280
+ });
281
+ },
282
+ };
283
+ });
284
+ }
285
+
286
+ // Re-export the flow helpers for tests + screens that need to reason about
287
+ // navigation without mutating the store.
288
+ export { livenessEnabled, nextStepAfter, previousStepBefore };
@@ -0,0 +1,78 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Server-driven config state (GET /api/kyc/config) — mirrors the Flutter SDK's
3
+ // serverConfig handling on KYCState. Holds the org's allowed (country, idType)
4
+ // list, per-ID feature flags, branding, and an error classification.
5
+ // ---------------------------------------------------------------------------
6
+
7
+ import { KYCApiError } from '../services/api';
8
+ import type { SdkConfigBranding, SdkConfigIdType } from '../services/api';
9
+
10
+ export type ServerConfigStatus = 'loading' | 'ready' | 'error';
11
+
12
+ export interface IdTypeFeatures {
13
+ documentVerification: boolean;
14
+ livenessCheck: boolean;
15
+ govDbCheck: boolean;
16
+ }
17
+
18
+ export interface ServerConfigState {
19
+ status: ServerConfigStatus;
20
+ idTypes: SdkConfigIdType[];
21
+ branding?: SdkConfigBranding;
22
+ environment?: 'DEVELOPMENT' | 'SANDBOX' | 'PRODUCTION';
23
+ /** HTTP status of a failed config fetch (if any). */
24
+ statusCode?: number;
25
+ /** A fatal failure (401/403) blocks the flow; non-fatal falls back to the prop list. */
26
+ fatal: boolean;
27
+ /** User-facing message for a fatal failure. */
28
+ message?: string;
29
+ }
30
+
31
+ export const INITIAL_SERVER_CONFIG: ServerConfigState = {
32
+ status: 'loading',
33
+ idTypes: [],
34
+ fatal: false,
35
+ };
36
+
37
+ /**
38
+ * Returns the per-ID feature flags for a `(country, idType)` pair, or `null`
39
+ * when the ID isn't granted or config hasn't loaded. Mirrors Flutter's
40
+ * `featuresFor` — callers must NOT replicate precedence logic elsewhere.
41
+ */
42
+ export function featuresFor(
43
+ config: ServerConfigState,
44
+ country: string,
45
+ idType: string,
46
+ ): IdTypeFeatures | null {
47
+ if (config.status !== 'ready') return null;
48
+ const match = config.idTypes.find((t) => t.country === country && t.idType === idType);
49
+ return match ? match.features : null;
50
+ }
51
+
52
+ /**
53
+ * Classifies a config-fetch error. 401 (invalid key) and 403 (not permitted) are
54
+ * FATAL — they block the flow and report to onError once. Everything else
55
+ * (network blips, 5xx) is non-fatal — the SDK trusts the consumer's `idTypes`
56
+ * prop and the server still 403s anything actually disabled. Mirrors Flutter's
57
+ * `_describeConfigError`.
58
+ */
59
+ export function describeConfigError(err: unknown): Pick<ServerConfigState, 'statusCode' | 'fatal' | 'message'> {
60
+ if (err instanceof KYCApiError) {
61
+ if (err.statusCode === 401) {
62
+ return {
63
+ statusCode: 401,
64
+ fatal: true,
65
+ message: 'Invalid API key. Please contact support.',
66
+ };
67
+ }
68
+ if (err.statusCode === 403) {
69
+ return {
70
+ statusCode: 403,
71
+ fatal: true,
72
+ message: "Your organization isn't permitted to start verification. Contact your administrator.",
73
+ };
74
+ }
75
+ return { statusCode: err.statusCode, fatal: false };
76
+ }
77
+ return { fatal: false };
78
+ }
@@ -0,0 +1,239 @@
1
+ import type { KYCSubmission, KYCError } from './verification';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Supported countries & ID types (identical set to the web + Flutter SDKs)
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export type SupportedCountry = 'NG' | 'GH' | 'KE' | 'ZA' | 'CI';
8
+
9
+ export type NigeriaIdType = 'bvn' | 'nin' | 'vnin' | 'passport' | 'drivers-license' | 'pvc';
10
+ export type GhanaIdType = 'ghana-card' | 'voters' | 'drivers-license' | 'ssnit' | 'passport';
11
+ export type KenyaIdType = 'national-id' | 'passport';
12
+ export type SouthAfricaIdType = 'national-id';
13
+ export type IvoryCoastIdType = 'cni' | 'residence-card';
14
+
15
+ export type IdType =
16
+ | NigeriaIdType
17
+ | GhanaIdType
18
+ | KenyaIdType
19
+ | SouthAfricaIdType
20
+ | IvoryCoastIdType;
21
+
22
+ /** Maps a country code to the ID types available in that country. */
23
+ export type IdTypeForCountry<C extends SupportedCountry> =
24
+ C extends 'NG' ? NigeriaIdType :
25
+ C extends 'GH' ? GhanaIdType :
26
+ C extends 'KE' ? KenyaIdType :
27
+ C extends 'ZA' ? SouthAfricaIdType :
28
+ C extends 'CI' ? IvoryCoastIdType :
29
+ never;
30
+
31
+ export interface IdTypeDefinition {
32
+ key: IdType;
33
+ label: string;
34
+ digits?: number;
35
+ pattern?: RegExp;
36
+ /** Whether this ID type requires photographing/uploading a physical document. */
37
+ requiresDocumentCapture: boolean;
38
+ /**
39
+ * How many sides of the document need to be scanned. Only present when
40
+ * `requiresDocumentCapture` is true.
41
+ * - `front_only` — single scan (passports, data-page only)
42
+ * - `front_and_back` — both sides required
43
+ */
44
+ scanSides?: 'front_only' | 'front_and_back';
45
+ }
46
+
47
+ export type IdTypesByCountry = {
48
+ [K in SupportedCountry]: readonly IdTypeDefinition[];
49
+ };
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // KYC flow steps
53
+ // ---------------------------------------------------------------------------
54
+
55
+ export type KYCStep =
56
+ | 'consent'
57
+ | 'id-type'
58
+ | 'id-input'
59
+ | 'document-capture'
60
+ | 'liveness'
61
+ | 'submitted';
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Appearance / theming
65
+ // ---------------------------------------------------------------------------
66
+
67
+ export interface KYCAppearance {
68
+ /** Brand color — drives buttons, selected states, focus rings. */
69
+ primaryColor?: string;
70
+ /** Text/icon color rendered on top of `primaryColor` (e.g. button labels). */
71
+ primaryTextColor?: string;
72
+ /** Accent color for subtle hover/active surfaces. */
73
+ accentColor?: string;
74
+ /** Modal/sheet background color. */
75
+ backgroundColor?: string;
76
+ /** Elevated surface color for cards/panels. */
77
+ surfaceColor?: string;
78
+ /** Border + input outline color. */
79
+ borderColor?: string;
80
+ /** Primary text color. */
81
+ textColor?: string;
82
+ companyName?: string;
83
+ /**
84
+ * Logo to show in the flow.
85
+ * - An image URL renders that logo.
86
+ * - The literal `'default'` renders the org's own logo from the server config
87
+ * response (falls back to the built-in shield if the org has none set).
88
+ * - Omitted renders the built-in shield badge.
89
+ */
90
+ logo?: string;
91
+ /** Initial light/dark mode. Applied on mount; the theme toggle can flip it. */
92
+ theme?: 'light' | 'dark';
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Consent / success screen content
97
+ // ---------------------------------------------------------------------------
98
+
99
+ export interface KYCConsentContent {
100
+ /** Consent heading. Supports `{firstName}` / `{lastName}` tokens. */
101
+ title?: string;
102
+ /** Sub-text under the heading. Same tokens. */
103
+ description?: string;
104
+ }
105
+
106
+ export interface KYCSuccessContent {
107
+ /** Success heading. Supports `{firstName}` / `{lastName}` tokens. */
108
+ title?: string;
109
+ /** Sub-text under the heading. Same tokens. */
110
+ description?: string;
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Voice guidance (spoken liveness instructions — TTS output only, no mic)
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /**
118
+ * Configuration for the spoken liveness instructions. TTS **output** for
119
+ * accessibility — it never records audio, so no microphone permission is
120
+ * involved. An object (not a bare boolean) so a `language` can be added later
121
+ * without a breaking change.
122
+ */
123
+ export interface VoiceGuidanceConfig {
124
+ /** Whether spoken guidance plays. Default `true`. */
125
+ enabled?: boolean;
126
+ /** BCP-47 voice tag (e.g. `'en-US'`, `'fr-FR'`). Default `'en-US'`. */
127
+ language?: string;
128
+ }
129
+
130
+ /** Accepts a bare boolean for ergonomics or the full {@link VoiceGuidanceConfig}. */
131
+ export type VoiceGuidanceOption = boolean | VoiceGuidanceConfig;
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Client-side SDK config (MyazaKYC.show() / useMyazaKYC options)
135
+ // ---------------------------------------------------------------------------
136
+
137
+ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
138
+ /**
139
+ * Bearer token. The key prefix is the single source of truth for the
140
+ * environment — the SDK derives it (and the base URL) automatically:
141
+ * `pk_dev_…` → development, `pk_test_…` → sandbox, `pk_live_…` → production.
142
+ * An unrecognized prefix throws.
143
+ */
144
+ apiKey: string;
145
+
146
+ /**
147
+ * Dev-only base-URL override. Only applied for **development** keys
148
+ * (`pk_dev_…`); defaults to a platform-aware localhost (`10.0.2.2:3001` on
149
+ * Android emulators, `localhost:3001` elsewhere). Ignored for sandbox /
150
+ * production keys.
151
+ */
152
+ devUrl?: string;
153
+
154
+ /** Two-letter country code. */
155
+ country: C;
156
+
157
+ /** Subset of ID types to offer. Only types valid for the country are accepted. */
158
+ idTypes?: IdTypeForCountry<C>[];
159
+
160
+ /** Pre-populated user data. Fields provided here won't be collected again. */
161
+ userData?: {
162
+ firstName?: string;
163
+ lastName?: string;
164
+ dateOfBirth?: string;
165
+ };
166
+
167
+ /** Enable the live-selfie capture step. */
168
+ enableSelfie?: boolean;
169
+
170
+ /** Enable the document-photo capture step. */
171
+ enableDocumentCapture?: boolean;
172
+
173
+ /**
174
+ * Allow picking a document photo from the device gallery as an alternative to
175
+ * the live camera capture. Default `true`. When `false`, the "upload a photo
176
+ * instead" affordances are hidden during normal capture.
177
+ */
178
+ allowDocumentUpload?: boolean;
179
+
180
+ /** Enable liveness detection during selfie capture. Default `true`. */
181
+ enableLiveness?: boolean;
182
+
183
+ /**
184
+ * Spoken liveness instructions (accessibility). `true`/omitted = on,
185
+ * `false` = off, or a {@link VoiceGuidanceConfig}. TTS output only — no
186
+ * microphone is used. Default: on.
187
+ */
188
+ voiceGuidance?: VoiceGuidanceOption;
189
+
190
+ /** Show a light/dark mode toggle button inside the modal header. Default `true`. */
191
+ showThemeToggle?: boolean;
192
+
193
+ /**
194
+ * Hide the close (X) button and block all user-initiated dismissal of the
195
+ * sheet — the X button, Android hardware back, and the iOS swipe-down drag.
196
+ * When `true`, the flow can only be closed programmatically via the `close()`
197
+ * returned by {@link useMyazaKYC}. Default `false`. The terminal "Submitted"
198
+ * step is already non-dismissible regardless of this flag.
199
+ */
200
+ disableClose?: boolean;
201
+
202
+ /** Visual customisation. */
203
+ appearance?: KYCAppearance;
204
+
205
+ /** Override the consent (welcome) screen copy. */
206
+ consent?: KYCConsentContent;
207
+
208
+ /** Override the success (submitted) screen copy. */
209
+ success?: KYCSuccessContent;
210
+
211
+ /** Arbitrary metadata forwarded with every verification request. */
212
+ metadata?: Record<string, string>;
213
+
214
+ // Callbacks
215
+ onStart?: () => void;
216
+ onStepChange?: (step: KYCStep) => void;
217
+ /**
218
+ * Fires immediately after the user submits. The submission is always
219
+ * status: 'pending' — results arrive async via webhook.
220
+ */
221
+ onSubmit?: (submission: KYCSubmission) => void;
222
+ onClose?: () => void;
223
+ /**
224
+ * Fires for technical errors only. Receives a {@link KYCError} carrying a
225
+ * typed `code`. Verification *outcomes* never come through here.
226
+ */
227
+ onError?: (error: KYCError) => void;
228
+ }
229
+
230
+ // ---------------------------------------------------------------------------
231
+ // useMyazaKYC() hook return type
232
+ // ---------------------------------------------------------------------------
233
+
234
+ export interface UseMyazaKYCReturn {
235
+ open: () => void;
236
+ close: () => void;
237
+ isOpen: boolean;
238
+ currentStep: KYCStep | null;
239
+ }
@@ -0,0 +1,6 @@
1
+ // country-flag-icons ships no types for its per-country `string/3x2/*` modules.
2
+ // Each default-exports the raw SVG markup as a string.
3
+ declare module 'country-flag-icons/string/3x2/*' {
4
+ const svg: string;
5
+ export default svg;
6
+ }