@myazahq/kyc-sdk-react-native 2.0.0 → 2.0.1

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 CHANGED
@@ -1,39 +1,67 @@
1
1
  # @myazahq/kyc-sdk-react-native
2
2
 
3
3
  Myaza KYC SDK for **React Native (Expo)** — ID verification, document capture, and
4
- active liveness detection. Mirrors the [web](https://www.npmjs.com/package/@myazahq/kyc-sdk-react)
4
+ active **on-device** liveness. Mirrors the [web](https://www.npmjs.com/package/@myazahq/kyc-sdk-react)
5
5
  and Flutter SDKs feature-for-feature and calls the same Myaza KYC API server.
6
6
 
7
7
  The SDK is a **thin UI layer**: it captures the user's data (ID number, document
8
8
  photos, a live selfie), uploads the media, and submits a verification request.
9
9
  All verification (OCR, facial comparison, gov-DB checks) happens server-side and
10
- is delivered asynchronously via webhook — the SDK is fire-and-forget.
10
+ is delivered asynchronously via webhook — the SDK is **fire-and-forget**.
11
+
12
+ ## Requirements
13
+
14
+ This library ships **native code** (an Apple Vision + Google ML Kit face detector,
15
+ built as a [react-native-vision-camera](https://react-native-vision-camera.com) v5 /
16
+ Nitro module), so it needs a **custom native build** and **does not run in Expo Go**.
17
+
18
+ | Requirement | Minimum |
19
+ | --- | --- |
20
+ | **iOS** deployment target | **15.1** |
21
+ | **Android** `minSdkVersion` | **24** (Android 7.0) · `compileSdk` 34 · NDK 27.1 |
22
+ | **Expo SDK** | **56** (React 19, React Native 0.85) |
23
+ | **React Native** | **0.83+**, with the **New Architecture enabled** (VisionCamera v5 / Nitro requires it; Expo SDK 56 enables it by default) |
24
+ | **Build toolchain** | Xcode + CocoaPods (iOS) · **JDK 17** for Android Gradle builds |
25
+ | **Runtime** | A **dev client** or bare build — **not Expo Go** |
26
+
27
+ **Peer dependencies** to install in your app:
28
+
29
+ | Package | Range | Purpose |
30
+ | --- | --- | --- |
31
+ | `expo` | `>=56` | Expo module runtime (the SDK uses several `expo-*` modules) |
32
+ | `react` / `react-native` | `>=19` / `>=0.83` | — |
33
+ | `react-native-vision-camera` | `^5` | Camera preview + capture |
34
+ | `react-native-vision-camera-worklets` | `>=5` | Frame-processor worklet runtime |
35
+ | `react-native-worklets` | `>=0.8` | Worklet `runOnJS` bridge for liveness |
36
+ | `react-native-nitro-modules` | `>=0.35` | Native module runtime for the face detector |
37
+ | `react-native-nitro-image` | `>=0.15` | Frame → image interop used by the detector |
38
+ | `react-native-safe-area-context` | `>=4` | Safe-area insets for the modal |
39
+ | `react-native-svg` | `>=15` | Liveness/overlay graphics |
11
40
 
12
- ## Install
13
-
14
- This library ships **native code** (an Apple Vision + Google ML Kit face-detector
15
- VisionCamera frame-processor plugin), so it needs a custom native build. It does
16
- **not** run in Expo Go. Pick your setup below.
41
+ > Voice guidance is **text-to-speech output only** — the SDK never records audio,
42
+ > so **no microphone permission** is requested or required.
17
43
 
18
- Peer dependencies (both setups): `react-native-vision-camera`,
19
- `react-native-worklets-core`, `react-native-svg`, and `expo` (the SDK uses several
20
- `expo-*` modules — see *Bare React Native* if you're not on a managed Expo app).
44
+ ## Installation
21
45
 
22
46
  ### Expo app (managed / prebuild — recommended)
23
47
 
24
48
  ```sh
25
49
  npx expo install @myazahq/kyc-sdk-react-native \
26
- react-native-vision-camera react-native-worklets-core react-native-svg
50
+ react-native-vision-camera react-native-vision-camera-worklets \
51
+ react-native-worklets react-native-nitro-modules react-native-nitro-image \
52
+ react-native-safe-area-context react-native-svg
27
53
  ```
28
54
 
29
- Add the config plugins to `app.json` (the SDK's plugin adds the iOS camera usage
55
+ Add the config plugins to `app.json`. The SDK's plugin adds the iOS camera usage
30
56
  string + Android `CAMERA`/`INTERNET` permissions; the VisionCamera plugin wires up
31
- the camera + frame processors):
57
+ the camera + frame processors. Make sure the **New Architecture** is on (it is by
58
+ default on Expo SDK 56):
32
59
 
33
60
  ```jsonc
34
61
  // app.json
35
62
  {
36
63
  "expo": {
64
+ "newArchEnabled": true,
37
65
  "plugins": [
38
66
  ["react-native-vision-camera", { "enableMicrophonePermission": false }],
39
67
  "@myazahq/kyc-sdk-react-native"
@@ -46,7 +74,8 @@ Then build a dev client (regenerates the native projects):
46
74
 
47
75
  ```sh
48
76
  npx expo prebuild
49
- npx expo run:ios # or: npx expo run:android
77
+ npx expo run:ios # iOS
78
+ JAVA_HOME=/path/to/jdk-17 npx expo run:android # Android — needs JDK 17
50
79
  ```
51
80
 
52
81
  > The SDK plugin accepts an optional custom camera prompt:
@@ -63,7 +92,9 @@ npx install-expo-modules@latest
63
92
 
64
93
  # 2. Install the SDK + peers:
65
94
  npm install @myazahq/kyc-sdk-react-native \
66
- react-native-vision-camera react-native-worklets-core react-native-svg \
95
+ react-native-vision-camera react-native-vision-camera-worklets \
96
+ react-native-worklets react-native-nitro-modules react-native-nitro-image \
97
+ react-native-safe-area-context react-native-svg \
67
98
  expo expo-image-manipulator expo-image-picker expo-speech expo-font \
68
99
  expo-glass-effect expo-application expo-crypto expo-device expo-localization
69
100
 
@@ -84,58 +115,285 @@ prebuild):
84
115
  <uses-permission android:name="android.permission.CAMERA" />
85
116
  <uses-permission android:name="android.permission.INTERNET" />
86
117
  ```
87
- - Enable VisionCamera frame processors (it needs `react-native-worklets-core`,
88
- installed above) — see the
118
+ - Ensure the **New Architecture** is enabled (`newArchEnabled=true` in
119
+ `android/gradle.properties`; `RCT_NEW_ARCH_ENABLED=1` for the iOS Podfile install),
120
+ and enable VisionCamera frame processors / the worklets babel plugin — see the
89
121
  [VisionCamera setup docs](https://react-native-vision-camera.com/docs/guides).
90
122
 
91
- The face-detector native module (podspec + Android Gradle project + the
92
- `detectFace` frame-processor plugin) is autolinked via React Native / Expo
93
- autolinking — no manual linking required.
94
-
95
- > Voice guidance is **text-to-speech output only** — the SDK never records audio,
96
- > so **no microphone permission** is requested or required
97
- > (`enableMicrophonePermission: false`).
123
+ The face-detector native module (Nitro HybridObject + Android lib loader) is
124
+ autolinked via React Native / Expo autolinking — no manual linking required.
98
125
 
99
126
  ## Usage
100
127
 
128
+ `<MyazaKYC />` renders a "Verify Identity" trigger plus the full modal flow. Pass
129
+ `children` (a string) to relabel it. For a fully custom trigger, use the
130
+ [`useMyazaKYC()` hook](#trigger-component--hook).
131
+
101
132
  ```tsx
102
133
  import { MyazaKYC } from '@myazahq/kyc-sdk-react-native';
103
134
 
104
- export default function Screen() {
135
+ export default function VerifyScreen() {
105
136
  return (
106
137
  <MyazaKYC
107
- apiKey="pk_test_xxx" // prefix selects the env: pk_test_ → sandbox
138
+ apiKey="pk_live_xxx" // prefix selects the env: pk_test_ → sandbox
108
139
  country="NG"
109
- idTypes={['bvn', 'nin', 'passport']}
140
+ idTypes={['passport', 'drivers-license', 'bvn', 'nin', 'pvc']}
141
+ userData={{ firstName: 'Jane', lastName: 'Doe' }}
142
+ enableSelfie
110
143
  enableDocumentCapture
111
144
  enableLiveness
112
- appearance={{ primaryColor: '#5645F5', companyName: 'Myaza', theme: 'light' }}
113
- onSubmit={(s) => console.log('submitted', s.verificationId)}
114
- onError={(e) => console.warn(e.code, e.message)}
115
- />
145
+ showThemeToggle
146
+ appearance={{
147
+ primaryColor: '#5645F5',
148
+ companyName: 'Myaza',
149
+ logo: 'default',
150
+ theme: 'dark',
151
+ }}
152
+ consent={{
153
+ title: 'Welcome, {firstName}',
154
+ description: "A quick check to confirm it's really you.",
155
+ }}
156
+ success={{
157
+ title: "You're all set, {firstName}!",
158
+ description: "We'll email you once your verification is reviewed.",
159
+ }}
160
+ metadata={{ userId: 'test_user_123' }}
161
+ onStart={() => console.log('KYC started')}
162
+ onStepChange={(step) => console.log('Step:', step)}
163
+ onSubmit={(submission) => {
164
+ // Fires as soon as the server accepts the request.
165
+ // submission.status is always 'pending' — the result arrives later via
166
+ // webhook to your backend (or poll GET /api/kyc/status/:id).
167
+ console.log('Submitted!', submission.verificationId);
168
+ }}
169
+ onClose={() => console.log('Modal closed')}
170
+ onError={(err) => console.warn('SDK error:', err.code, err.message)}
171
+ >
172
+ Verify my identity
173
+ </MyazaKYC>
116
174
  );
117
175
  }
118
176
  ```
119
177
 
120
- Or drive the flow from your own trigger with the hook:
121
-
122
- ```tsx
123
- import { useMyazaKYC } from '@myazahq/kyc-sdk-react-native';
124
-
125
- const { open, close, isOpen, currentStep } = useMyazaKYC({ apiKey, country: 'NG' });
126
- ```
178
+ ## Props
179
+
180
+ | Prop | Type | Default | Description |
181
+ | ----------------------- | ----------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------- |
182
+ | `apiKey` | `string` | — | **Required.** Sent as `Authorization: Bearer`. The **environment is derived from the key prefix** (`pk_test_…` → sandbox, `pk_live_…` → production); an unrecognized prefix throws. |
183
+ | `country` | `'NG' \| 'GH' \| 'KE' \| 'ZA' \| 'CI'` | — | **Required.** Country whose ID types are offered. |
184
+ | `idTypes` | `IdType[]` | all allowed for org | Subset of ID types to offer; must be valid for `country`. |
185
+ | `userData` | `{ firstName?, lastName?, dateOfBirth? }` | — | Pre-fills the user's details. |
186
+ | `enableSelfie` | `boolean` | `true` | Capture a selfie during liveness. |
187
+ | `enableDocumentCapture` | `boolean` | `true` | Enable the document-scan step for document IDs. |
188
+ | `allowDocumentUpload` | `boolean` | `true` | Allow picking a document photo from the gallery as an alternative to the camera. `false` hides every "upload instead" affordance (it's still offered on the camera-permission-denied screen as an escape hatch). |
189
+ | `enableLiveness` | `boolean` | `true` | Run the liveness challenge step. The server can still disable it per ID type. |
190
+ | `voiceGuidance` | `boolean \| { enabled?, language? }` | `true` | Spoken liveness instructions (accessibility, TTS **output** — no microphone). `false` mutes it; pass `{ language: 'fr-FR' }` to set the voice. See [Robustness & error handling](#robustness--error-handling). |
191
+ | `showThemeToggle` | `boolean` | `true` | Show a light/dark toggle inside the modal header. When `false`, the flow stays on `appearance.theme`. |
192
+ | `disableClose` | `boolean` | `false` | Hide the close (X) and block **all** user dismissal (X, Android back, iOS swipe-down). The flow can then only be closed programmatically via `useMyazaKYC().close()`. |
193
+ | `appearance` | `KYCAppearance` | brand defaults | Brand & theme the modal — colors, logo, light/dark. See [Appearance & theming](#appearance--theming). |
194
+ | `consent` | `KYCConsentContent` | built-in copy | Override the consent/welcome screen `title` and `description`. See [Consent screen copy](#consent-screen-copy). |
195
+ | `success` | `KYCSuccessContent` | built-in copy | Override the success/submitted screen `title` and `description`. See [Success screen copy](#success-screen-copy). |
196
+ | `metadata` | `Record<string, string>` | — | Forwarded with every verify request. |
197
+ | `onStart` | `() => void` | — | Called when the flow opens. |
198
+ | `onStepChange` | `(step: KYCStep) => void` | — | Called on each step transition. |
199
+ | `onSubmit` | `(submission: KYCSubmission) => void` | — | Called when the server accepts the verification. `status` is always `'pending'`. |
200
+ | `onError` | `(error: KYCError) => void` | — | Called for **technical** errors only. Receives a typed [`KYCError`](#robustness--error-handling). Verification outcomes never come through here. |
201
+ | `onClose` | `() => void` | — | Called when the user closes the flow. |
202
+ | `children` | `string` | `Verify Identity` | Trigger label. Defaults to `Verify with {companyName}` when `companyName` is set, else `Verify Identity`. |
203
+ | `disabled` | `boolean` | `false` | Disable the trigger. |
127
204
 
128
205
  ## Environment
129
206
 
130
- The base URL is auto-detected from the API key prefix — there is **no**
131
- `environment` prop:
207
+ There is **no `environment` prop** — the SDK derives the environment (and the
208
+ base URL) from the API key prefix, the single source of truth:
132
209
 
133
210
  | Prefix | Environment | Base URL |
134
211
  | ---------- | ----------- | --------------------------------- |
135
- | `pk_dev_` | development | `http://localhost:3001` (override with `devUrl`) |
136
212
  | `pk_test_` | sandbox | `https://sandbox.identity.myaza.app` |
137
213
  | `pk_live_` | production | `https://identity.myaza.app` |
138
214
 
215
+ An unrecognized or malformed key throws at setup (it never silently defaults).
216
+
217
+ ## Trigger component & hook
218
+
219
+ `<MyazaKYC />` renders a styled trigger that opens the modal. Beyond the config
220
+ props it accepts `children` (a string label) and `disabled`:
221
+
222
+ ```tsx
223
+ <MyazaKYC {...config} disabled={!ready}>
224
+ Start verification
225
+ </MyazaKYC>
226
+ ```
227
+
228
+ For a **fully custom** trigger (your own `Pressable`, an icon, a list row, or to
229
+ open the flow programmatically), use the `useMyazaKYC()` hook:
230
+
231
+ ```tsx
232
+ import { Pressable, Text } from 'react-native';
233
+ import { useMyazaKYC } from '@myazahq/kyc-sdk-react-native';
234
+
235
+ function CustomTrigger() {
236
+ const { open, close, isOpen, currentStep } = useMyazaKYC({
237
+ apiKey: 'pk_live_xxx',
238
+ country: 'NG',
239
+ onSubmit: (s) => console.log('submitted', s.verificationId),
240
+ });
241
+
242
+ return (
243
+ <Pressable onPress={open}>
244
+ <Text>Verify your identity</Text>
245
+ </Pressable>
246
+ );
247
+ }
248
+ ```
249
+
250
+ `useMyazaKYC(config)` returns `{ open, close, isOpen, currentStep }`. `close()` is
251
+ the only way to dismiss the flow when `disableClose` is set.
252
+
253
+ ## Appearance & theming
254
+
255
+ Pass an `appearance` object to brand the flow. Because the UI is token-driven,
256
+ setting one color cascades to all of its shades (hover/selected/focus states
257
+ included). Unset colors keep the built-in light/dark defaults.
258
+
259
+ | Field | Type | Description |
260
+ | ------------------ | ------------------- | -------------------------------------------------------------------------------- |
261
+ | `primaryColor` | `string` | Brand color — buttons, selected states, progress, the shield hero. Defaults to `#5645F5`. |
262
+ | `primaryTextColor` | `string` | Text/icons rendered on top of `primaryColor` (e.g. button labels). |
263
+ | `accentColor` | `string` | Subtle hover/active surfaces. |
264
+ | `backgroundColor` | `string` | Modal background. |
265
+ | `surfaceColor` | `string` | Cards & panels. |
266
+ | `borderColor` | `string` | Borders and input outlines. |
267
+ | `textColor` | `string` | Primary text color. |
268
+ | `companyName` | `string` | Used on the verify trigger ("Verify with …") and the persistent header. |
269
+ | `logo` | `string` | Image URL, or `'default'` to use your org's logo. See below. |
270
+ | `theme` | `'light' \| 'dark'` | Initial mode (defaults to `'light'`). With `showThemeToggle`, users can flip it. |
271
+
272
+ ### Logo
273
+
274
+ The org logo renders as a small circular avatar in the modal header (top-left),
275
+ persistent on every step, alongside `companyName`.
276
+
277
+ - `logo: 'https://…/logo.png'` — uses that image directly.
278
+ - `logo: 'default'` — pulls your organization's logo configured in the **Myaza dashboard**
279
+ (returned by the server on mount). If your org has no logo set, or the image fails
280
+ to load, the avatar is hidden.
281
+ - omitted — no header logo.
282
+
283
+ ```tsx
284
+ appearance={{
285
+ primaryColor: '#0F7B6C',
286
+ primaryTextColor: '#FFFFFF',
287
+ surfaceColor: '#F4F7F6',
288
+ borderColor: '#D7E3E0',
289
+ logo: 'default',
290
+ theme: 'light',
291
+ }}
292
+ ```
293
+
294
+ ## Consent screen copy
295
+
296
+ The welcome/consent step shows a heading and a short description. Override either
297
+ through the `consent` prop:
298
+
299
+ | Field | Type | Description |
300
+ | ------------- | -------- | ----------------------------------------------------------------------------------------------------- |
301
+ | `title` | `string` | Heading. Defaults to `Welcome, {firstName}` when a first name is known, else `Identity Verification`. |
302
+ | `description` | `string` | Sub-text under the heading. Defaults to the built-in regulatory copy. |
303
+
304
+ Both fields support `{firstName}` and `{lastName}` tokens, replaced with the values
305
+ from `userData` (empty string when absent), so a custom title can still greet the
306
+ user by name.
307
+
308
+ ```tsx
309
+ consent={{
310
+ title: 'Welcome, {firstName}',
311
+ description: "We just need to confirm it's really you. This takes about a minute.",
312
+ }}
313
+ ```
314
+
315
+ ## Success screen copy
316
+
317
+ After the user submits, the final screen shows a confirmation heading and
318
+ description. Override either through the `success` prop:
319
+
320
+ | Field | Type | Description |
321
+ | ------------- | -------- | -------------------------------------------------------------------------- |
322
+ | `title` | `string` | Heading. Defaults to `Verification Submitted!`. |
323
+ | `description` | `string` | Sub-text under the heading. Defaults to the built-in "submitted for review" copy. |
324
+
325
+ Both fields support the same `{firstName}` / `{lastName}` tokens as `consent`.
326
+
327
+ ```tsx
328
+ success={{
329
+ title: "You're all set, {firstName}!",
330
+ description: "We'll email you once your verification is reviewed.",
331
+ }}
332
+ ```
333
+
334
+ ## Robustness & error handling
335
+
336
+ The SDK is resilient to flaky networks, denied permissions, and poor capture
337
+ conditions, and reports technical failures through `onError` with a typed code.
338
+
339
+ ### Typed errors (`onError`)
340
+
341
+ `onError` receives a `KYCError` with a typed `code`, a human-readable `message`,
342
+ and optional `details`. The codes are **identical to the web and Flutter SDKs**:
343
+
344
+ ```tsx
345
+ import { MyazaKYC, type KYCError } from '@myazahq/kyc-sdk-react-native';
346
+
347
+ <MyazaKYC
348
+ {...config}
349
+ onError={(error: KYCError) => {
350
+ switch (error.code) {
351
+ case 'camera_permission_denied': /* ask the user to allow the camera */ break;
352
+ case 'insufficient_credits': /* error.details = { required, balance, currency } */ break;
353
+ case 'network_error':
354
+ case 'upload_failed': /* shown only after automatic retries */ break;
355
+ }
356
+ }}
357
+ />
358
+ ```
359
+
360
+ | `code` | When it fires |
361
+ | -------------------------- | ------------------------------------------------------------------- |
362
+ | `network_error` | Connection failure / timeout, **after retries are exhausted**. |
363
+ | `invalid_api_key` | Server returned `401`. |
364
+ | `insufficient_credits` | Server returned `402`. `details = { required, balance, currency }`. |
365
+ | `upload_failed` | A media upload failed, **after retries are exhausted**. |
366
+ | `camera_permission_denied` | The user denied (or the OS blocks) camera access. |
367
+ | `feature_disabled` | Server returned `403` (ID type / feature not enabled for the org). |
368
+ | `unknown` | Anything else. |
369
+
370
+ > Verification *outcomes* (identity not found, document mismatch, …) never come
371
+ > through `onError` — they arrive asynchronously via webhook / `GET /api/kyc/status/:id`.
372
+
373
+ ### Network resilience
374
+
375
+ Media uploads and the verify submission are wrapped in exponential-backoff retry
376
+ (with jitter), retrying only *transient* failures (network / timeout / `5xx`);
377
+ terminal `4xx` surface immediately. The UI shows a top toast while retrying, and
378
+ `onError` fires **only after retries are exhausted** (`upload_failed` for uploads,
379
+ `network_error` for connectivity).
380
+
381
+ ### Camera permission
382
+
383
+ If the user denies camera access, the SDK shows a clear "camera access needed"
384
+ screen (with an **Open Settings** action) instead of hanging, and reports
385
+ `camera_permission_denied` to `onError`. Document capture additionally offers a
386
+ gallery-upload fallback unless `allowDocumentUpload` is `false`.
387
+
388
+ ### Liveness quality guards
389
+
390
+ - **Multiple faces** — if more than one face is in frame, the challenge pauses
391
+ ("Make sure only your face is visible") and resumes automatically when only one
392
+ face remains. This guards capture quality and a class of spoofing.
393
+ - **Lighting** — too-dark *and* too-bright (glare) conditions are detected live
394
+ during liveness; the SDK shows guidance ("Move to a brighter area" / "Too bright
395
+ — reduce glare") and blocks auto-capture until lighting is acceptable.
396
+
139
397
  ## Liveness
140
398
 
141
399
  Active, challenge-based liveness (nod / turn / blink / smile — 2 randomly chosen
@@ -147,18 +405,14 @@ per session). Face detection runs **on-device, natively**:
147
405
  simulators).
148
406
 
149
407
  Both run as a [react-native-vision-camera](https://github.com/mrousavy/react-native-vision-camera)
150
- frame-processor plugin: the camera frame never crosses the JS bridge. The selfie
151
- is **auto-captured** once challenges pass (anti-spoofing), and a short liveness
152
- video is recorded and uploaded best-effort.
408
+ v5 Nitro frame processor: the camera frame never crosses the JS bridge. The selfie
409
+ is **auto-captured** once challenges pass (anti-spoofing — never user-triggered),
410
+ and a short liveness video is recorded and uploaded best-effort.
153
411
 
154
- ## Callbacks
412
+ ## Documentation
155
413
 
156
- `onSubmit(submission)` fires the moment the server accepts the request
157
- (`status: 'pending'`). `onError(KYCError)` fires for **technical** errors only
158
- (`network_error`, `invalid_api_key`, `insufficient_credits`, `upload_failed`,
159
- `camera_permission_denied`, `feature_disabled`). Verification *outcomes* arrive
160
- asynchronously via webhook / `GET /api/kyc/status/:id` — never through `onError`.
414
+ Full documentation, configuration options, and webhook setup: **[identity.myaza.co/documentation/sdks](https://identity.myaza.co/documentation/sdks)**.
161
415
 
162
416
  ## License
163
417
 
164
- UNLICENSED — © Myaza.
418
+ MIT © Flitstack Technologies Inc.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myazahq/kyc-sdk-react-native",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Myaza KYC SDK for React Native (Expo) — ID verification, liveness detection, and document capture",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,7 +14,7 @@ export type DeviceType = 'mobile' | 'tablet' | 'desktop' | 'unknown';
14
14
  * Single source of truth for the SDK version — also used by `services/api.ts`
15
15
  * for the `X-SDK-Version` header. Keep in sync with `package.json`.
16
16
  */
17
- export const SDK_VERSION = '2.0.0';
17
+ export const SDK_VERSION = '2.0.1';
18
18
 
19
19
  export interface ReactNativeDeviceMetadata {
20
20
  sdkType: 'react-native';