@kwik-id/sdk-react-native 0.0.0-stage → 0.1.0-alpha.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 (269) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +81 -2
  3. package/app.plugin.js +40 -0
  4. package/lib/commonjs/api/api-client.js +213 -0
  5. package/lib/commonjs/api/api-client.js.map +1 -0
  6. package/lib/commonjs/api/api-error.js +29 -0
  7. package/lib/commonjs/api/api-error.js.map +1 -0
  8. package/lib/commonjs/api/index.js +33 -0
  9. package/lib/commonjs/api/index.js.map +1 -0
  10. package/lib/commonjs/api/upload.js +74 -0
  11. package/lib/commonjs/api/upload.js.map +1 -0
  12. package/lib/commonjs/camera/camera-view.js +227 -0
  13. package/lib/commonjs/camera/camera-view.js.map +1 -0
  14. package/lib/commonjs/camera/index.js +20 -0
  15. package/lib/commonjs/camera/index.js.map +1 -0
  16. package/lib/commonjs/camera/photo-capture.js +50 -0
  17. package/lib/commonjs/camera/photo-capture.js.map +1 -0
  18. package/lib/commonjs/crypto/fingerprint.js +43 -0
  19. package/lib/commonjs/crypto/fingerprint.js.map +1 -0
  20. package/lib/commonjs/crypto/hmac.js +150 -0
  21. package/lib/commonjs/crypto/hmac.js.map +1 -0
  22. package/lib/commonjs/crypto/index.js +39 -0
  23. package/lib/commonjs/crypto/index.js.map +1 -0
  24. package/lib/commonjs/crypto/session-manager.js +173 -0
  25. package/lib/commonjs/crypto/session-manager.js.map +1 -0
  26. package/lib/commonjs/i18n/en.js +297 -0
  27. package/lib/commonjs/i18n/en.js.map +1 -0
  28. package/lib/commonjs/i18n/fr.js +294 -0
  29. package/lib/commonjs/i18n/fr.js.map +1 -0
  30. package/lib/commonjs/i18n/index.js +34 -0
  31. package/lib/commonjs/i18n/index.js.map +1 -0
  32. package/lib/commonjs/i18n/locale.js +20 -0
  33. package/lib/commonjs/i18n/locale.js.map +1 -0
  34. package/lib/commonjs/i18n/translator.js +37 -0
  35. package/lib/commonjs/i18n/translator.js.map +1 -0
  36. package/lib/commonjs/index.js +187 -0
  37. package/lib/commonjs/index.js.map +1 -0
  38. package/lib/commonjs/lib/device-detector.js +34 -0
  39. package/lib/commonjs/lib/device-detector.js.map +1 -0
  40. package/lib/commonjs/lib/event-emitter.js +85 -0
  41. package/lib/commonjs/lib/event-emitter.js.map +1 -0
  42. package/lib/commonjs/lib/index.js +63 -0
  43. package/lib/commonjs/lib/index.js.map +1 -0
  44. package/lib/commonjs/lib/kwik-id-rn-sdk.js +156 -0
  45. package/lib/commonjs/lib/kwik-id-rn-sdk.js.map +1 -0
  46. package/lib/commonjs/lib/verification-machine.js +276 -0
  47. package/lib/commonjs/lib/verification-machine.js.map +1 -0
  48. package/lib/commonjs/package.json +1 -0
  49. package/lib/commonjs/types/events.js +6 -0
  50. package/lib/commonjs/types/events.js.map +1 -0
  51. package/lib/commonjs/types/index.js +2 -0
  52. package/lib/commonjs/types/index.js.map +1 -0
  53. package/lib/commonjs/ui/components/KwikIDVerificationModal.js +407 -0
  54. package/lib/commonjs/ui/components/KwikIDVerificationModal.js.map +1 -0
  55. package/lib/commonjs/ui/components/screens.js +338 -0
  56. package/lib/commonjs/ui/components/screens.js.map +1 -0
  57. package/lib/commonjs/ui/context/kwik-id-provider.js +52 -0
  58. package/lib/commonjs/ui/context/kwik-id-provider.js.map +1 -0
  59. package/lib/commonjs/ui/hooks/use-kwik-verification.js +77 -0
  60. package/lib/commonjs/ui/hooks/use-kwik-verification.js.map +1 -0
  61. package/lib/commonjs/ui/index.js +33 -0
  62. package/lib/commonjs/ui/index.js.map +1 -0
  63. package/lib/module/api/api-client.js +208 -0
  64. package/lib/module/api/api-client.js.map +1 -0
  65. package/lib/module/api/api-error.js +24 -0
  66. package/lib/module/api/api-error.js.map +1 -0
  67. package/lib/module/api/index.js +6 -0
  68. package/lib/module/api/index.js.map +1 -0
  69. package/lib/module/api/upload.js +68 -0
  70. package/lib/module/api/upload.js.map +1 -0
  71. package/lib/module/camera/camera-view.js +222 -0
  72. package/lib/module/camera/camera-view.js.map +1 -0
  73. package/lib/module/camera/index.js +5 -0
  74. package/lib/module/camera/index.js.map +1 -0
  75. package/lib/module/camera/photo-capture.js +45 -0
  76. package/lib/module/camera/photo-capture.js.map +1 -0
  77. package/lib/module/crypto/fingerprint.js +37 -0
  78. package/lib/module/crypto/fingerprint.js.map +1 -0
  79. package/lib/module/crypto/hmac.js +143 -0
  80. package/lib/module/crypto/hmac.js.map +1 -0
  81. package/lib/module/crypto/index.js +6 -0
  82. package/lib/module/crypto/index.js.map +1 -0
  83. package/lib/module/crypto/session-manager.js +169 -0
  84. package/lib/module/crypto/session-manager.js.map +1 -0
  85. package/lib/module/i18n/en.js +293 -0
  86. package/lib/module/i18n/en.js.map +1 -0
  87. package/lib/module/i18n/fr.js +290 -0
  88. package/lib/module/i18n/fr.js.map +1 -0
  89. package/lib/module/i18n/index.js +11 -0
  90. package/lib/module/i18n/index.js.map +1 -0
  91. package/lib/module/i18n/locale.js +15 -0
  92. package/lib/module/i18n/locale.js.map +1 -0
  93. package/lib/module/i18n/translator.js +34 -0
  94. package/lib/module/i18n/translator.js.map +1 -0
  95. package/lib/module/index.js +30 -0
  96. package/lib/module/index.js.map +1 -0
  97. package/lib/module/lib/device-detector.js +26 -0
  98. package/lib/module/lib/device-detector.js.map +1 -0
  99. package/lib/module/lib/event-emitter.js +80 -0
  100. package/lib/module/lib/event-emitter.js.map +1 -0
  101. package/lib/module/lib/index.js +6 -0
  102. package/lib/module/lib/index.js.map +1 -0
  103. package/lib/module/lib/kwik-id-rn-sdk.js +150 -0
  104. package/lib/module/lib/kwik-id-rn-sdk.js.map +1 -0
  105. package/lib/module/lib/verification-machine.js +268 -0
  106. package/lib/module/lib/verification-machine.js.map +1 -0
  107. package/lib/module/package.json +1 -0
  108. package/lib/module/types/events.js +4 -0
  109. package/lib/module/types/events.js.map +1 -0
  110. package/lib/module/types/index.js +2 -0
  111. package/lib/module/types/index.js.map +1 -0
  112. package/lib/module/ui/components/KwikIDVerificationModal.js +402 -0
  113. package/lib/module/ui/components/KwikIDVerificationModal.js.map +1 -0
  114. package/lib/module/ui/components/screens.js +328 -0
  115. package/lib/module/ui/components/screens.js.map +1 -0
  116. package/lib/module/ui/context/kwik-id-provider.js +46 -0
  117. package/lib/module/ui/context/kwik-id-provider.js.map +1 -0
  118. package/lib/module/ui/hooks/use-kwik-verification.js +72 -0
  119. package/lib/module/ui/hooks/use-kwik-verification.js.map +1 -0
  120. package/lib/module/ui/index.js +6 -0
  121. package/lib/module/ui/index.js.map +1 -0
  122. package/lib/typescript/commonjs/api/api-client.d.ts +82 -0
  123. package/lib/typescript/commonjs/api/api-client.d.ts.map +1 -0
  124. package/lib/typescript/commonjs/api/api-error.d.ts +13 -0
  125. package/lib/typescript/commonjs/api/api-error.d.ts.map +1 -0
  126. package/lib/typescript/commonjs/api/index.d.ts +6 -0
  127. package/lib/typescript/commonjs/api/index.d.ts.map +1 -0
  128. package/lib/typescript/commonjs/api/upload.d.ts +19 -0
  129. package/lib/typescript/commonjs/api/upload.d.ts.map +1 -0
  130. package/lib/typescript/commonjs/camera/camera-view.d.ts +25 -0
  131. package/lib/typescript/commonjs/camera/camera-view.d.ts.map +1 -0
  132. package/lib/typescript/commonjs/camera/index.d.ts +5 -0
  133. package/lib/typescript/commonjs/camera/index.d.ts.map +1 -0
  134. package/lib/typescript/commonjs/camera/photo-capture.d.ts +9 -0
  135. package/lib/typescript/commonjs/camera/photo-capture.d.ts.map +1 -0
  136. package/lib/typescript/commonjs/crypto/fingerprint.d.ts +3 -0
  137. package/lib/typescript/commonjs/crypto/fingerprint.d.ts.map +1 -0
  138. package/lib/typescript/commonjs/crypto/hmac.d.ts +18 -0
  139. package/lib/typescript/commonjs/crypto/hmac.d.ts.map +1 -0
  140. package/lib/typescript/commonjs/crypto/index.d.ts +4 -0
  141. package/lib/typescript/commonjs/crypto/index.d.ts.map +1 -0
  142. package/lib/typescript/commonjs/crypto/session-manager.d.ts +27 -0
  143. package/lib/typescript/commonjs/crypto/session-manager.d.ts.map +1 -0
  144. package/lib/typescript/commonjs/i18n/en.d.ts +291 -0
  145. package/lib/typescript/commonjs/i18n/en.d.ts.map +1 -0
  146. package/lib/typescript/commonjs/i18n/fr.d.ts +6 -0
  147. package/lib/typescript/commonjs/i18n/fr.d.ts.map +1 -0
  148. package/lib/typescript/commonjs/i18n/index.d.ts +10 -0
  149. package/lib/typescript/commonjs/i18n/index.d.ts.map +1 -0
  150. package/lib/typescript/commonjs/i18n/locale.d.ts +4 -0
  151. package/lib/typescript/commonjs/i18n/locale.d.ts.map +1 -0
  152. package/lib/typescript/commonjs/i18n/translator.d.ts +15 -0
  153. package/lib/typescript/commonjs/i18n/translator.d.ts.map +1 -0
  154. package/lib/typescript/commonjs/index.d.ts +24 -0
  155. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  156. package/lib/typescript/commonjs/lib/device-detector.d.ts +5 -0
  157. package/lib/typescript/commonjs/lib/device-detector.d.ts.map +1 -0
  158. package/lib/typescript/commonjs/lib/event-emitter.d.ts +19 -0
  159. package/lib/typescript/commonjs/lib/event-emitter.d.ts.map +1 -0
  160. package/lib/typescript/commonjs/lib/index.d.ts +5 -0
  161. package/lib/typescript/commonjs/lib/index.d.ts.map +1 -0
  162. package/lib/typescript/commonjs/lib/kwik-id-rn-sdk.d.ts +43 -0
  163. package/lib/typescript/commonjs/lib/kwik-id-rn-sdk.d.ts.map +1 -0
  164. package/lib/typescript/commonjs/lib/verification-machine.d.ts +78 -0
  165. package/lib/typescript/commonjs/lib/verification-machine.d.ts.map +1 -0
  166. package/lib/typescript/commonjs/package.json +1 -0
  167. package/lib/typescript/commonjs/types/events.d.ts +94 -0
  168. package/lib/typescript/commonjs/types/events.d.ts.map +1 -0
  169. package/lib/typescript/commonjs/types/index.d.ts +242 -0
  170. package/lib/typescript/commonjs/types/index.d.ts.map +1 -0
  171. package/lib/typescript/commonjs/ui/components/KwikIDVerificationModal.d.ts +21 -0
  172. package/lib/typescript/commonjs/ui/components/KwikIDVerificationModal.d.ts.map +1 -0
  173. package/lib/typescript/commonjs/ui/components/screens.d.ts +46 -0
  174. package/lib/typescript/commonjs/ui/components/screens.d.ts.map +1 -0
  175. package/lib/typescript/commonjs/ui/context/kwik-id-provider.d.ts +19 -0
  176. package/lib/typescript/commonjs/ui/context/kwik-id-provider.d.ts.map +1 -0
  177. package/lib/typescript/commonjs/ui/hooks/use-kwik-verification.d.ts +29 -0
  178. package/lib/typescript/commonjs/ui/hooks/use-kwik-verification.d.ts.map +1 -0
  179. package/lib/typescript/commonjs/ui/index.d.ts +7 -0
  180. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -0
  181. package/lib/typescript/module/api/api-client.d.ts +82 -0
  182. package/lib/typescript/module/api/api-client.d.ts.map +1 -0
  183. package/lib/typescript/module/api/api-error.d.ts +13 -0
  184. package/lib/typescript/module/api/api-error.d.ts.map +1 -0
  185. package/lib/typescript/module/api/index.d.ts +6 -0
  186. package/lib/typescript/module/api/index.d.ts.map +1 -0
  187. package/lib/typescript/module/api/upload.d.ts +19 -0
  188. package/lib/typescript/module/api/upload.d.ts.map +1 -0
  189. package/lib/typescript/module/camera/camera-view.d.ts +25 -0
  190. package/lib/typescript/module/camera/camera-view.d.ts.map +1 -0
  191. package/lib/typescript/module/camera/index.d.ts +5 -0
  192. package/lib/typescript/module/camera/index.d.ts.map +1 -0
  193. package/lib/typescript/module/camera/photo-capture.d.ts +9 -0
  194. package/lib/typescript/module/camera/photo-capture.d.ts.map +1 -0
  195. package/lib/typescript/module/crypto/fingerprint.d.ts +3 -0
  196. package/lib/typescript/module/crypto/fingerprint.d.ts.map +1 -0
  197. package/lib/typescript/module/crypto/hmac.d.ts +18 -0
  198. package/lib/typescript/module/crypto/hmac.d.ts.map +1 -0
  199. package/lib/typescript/module/crypto/index.d.ts +4 -0
  200. package/lib/typescript/module/crypto/index.d.ts.map +1 -0
  201. package/lib/typescript/module/crypto/session-manager.d.ts +27 -0
  202. package/lib/typescript/module/crypto/session-manager.d.ts.map +1 -0
  203. package/lib/typescript/module/i18n/en.d.ts +291 -0
  204. package/lib/typescript/module/i18n/en.d.ts.map +1 -0
  205. package/lib/typescript/module/i18n/fr.d.ts +6 -0
  206. package/lib/typescript/module/i18n/fr.d.ts.map +1 -0
  207. package/lib/typescript/module/i18n/index.d.ts +10 -0
  208. package/lib/typescript/module/i18n/index.d.ts.map +1 -0
  209. package/lib/typescript/module/i18n/locale.d.ts +4 -0
  210. package/lib/typescript/module/i18n/locale.d.ts.map +1 -0
  211. package/lib/typescript/module/i18n/translator.d.ts +15 -0
  212. package/lib/typescript/module/i18n/translator.d.ts.map +1 -0
  213. package/lib/typescript/module/index.d.ts +24 -0
  214. package/lib/typescript/module/index.d.ts.map +1 -0
  215. package/lib/typescript/module/lib/device-detector.d.ts +5 -0
  216. package/lib/typescript/module/lib/device-detector.d.ts.map +1 -0
  217. package/lib/typescript/module/lib/event-emitter.d.ts +19 -0
  218. package/lib/typescript/module/lib/event-emitter.d.ts.map +1 -0
  219. package/lib/typescript/module/lib/index.d.ts +5 -0
  220. package/lib/typescript/module/lib/index.d.ts.map +1 -0
  221. package/lib/typescript/module/lib/kwik-id-rn-sdk.d.ts +43 -0
  222. package/lib/typescript/module/lib/kwik-id-rn-sdk.d.ts.map +1 -0
  223. package/lib/typescript/module/lib/verification-machine.d.ts +78 -0
  224. package/lib/typescript/module/lib/verification-machine.d.ts.map +1 -0
  225. package/lib/typescript/module/package.json +1 -0
  226. package/lib/typescript/module/types/events.d.ts +94 -0
  227. package/lib/typescript/module/types/events.d.ts.map +1 -0
  228. package/lib/typescript/module/types/index.d.ts +242 -0
  229. package/lib/typescript/module/types/index.d.ts.map +1 -0
  230. package/lib/typescript/module/ui/components/KwikIDVerificationModal.d.ts +21 -0
  231. package/lib/typescript/module/ui/components/KwikIDVerificationModal.d.ts.map +1 -0
  232. package/lib/typescript/module/ui/components/screens.d.ts +46 -0
  233. package/lib/typescript/module/ui/components/screens.d.ts.map +1 -0
  234. package/lib/typescript/module/ui/context/kwik-id-provider.d.ts +19 -0
  235. package/lib/typescript/module/ui/context/kwik-id-provider.d.ts.map +1 -0
  236. package/lib/typescript/module/ui/hooks/use-kwik-verification.d.ts +29 -0
  237. package/lib/typescript/module/ui/hooks/use-kwik-verification.d.ts.map +1 -0
  238. package/lib/typescript/module/ui/index.d.ts +7 -0
  239. package/lib/typescript/module/ui/index.d.ts.map +1 -0
  240. package/package.json +121 -3
  241. package/src/api/api-client.ts +319 -0
  242. package/src/api/api-error.ts +28 -0
  243. package/src/api/index.ts +12 -0
  244. package/src/api/upload.ts +87 -0
  245. package/src/camera/camera-view.tsx +217 -0
  246. package/src/camera/index.ts +4 -0
  247. package/src/camera/photo-capture.ts +58 -0
  248. package/src/crypto/fingerprint.ts +46 -0
  249. package/src/crypto/hmac.ts +172 -0
  250. package/src/crypto/index.ts +3 -0
  251. package/src/crypto/session-manager.ts +200 -0
  252. package/src/i18n/en.ts +315 -0
  253. package/src/i18n/fr.ts +325 -0
  254. package/src/i18n/index.ts +9 -0
  255. package/src/i18n/locale.ts +15 -0
  256. package/src/i18n/translator.ts +37 -0
  257. package/src/index.ts +102 -0
  258. package/src/lib/device-detector.ts +32 -0
  259. package/src/lib/event-emitter.ts +90 -0
  260. package/src/lib/index.ts +15 -0
  261. package/src/lib/kwik-id-rn-sdk.ts +160 -0
  262. package/src/lib/verification-machine.ts +273 -0
  263. package/src/types/events.ts +75 -0
  264. package/src/types/index.ts +309 -0
  265. package/src/ui/components/KwikIDVerificationModal.tsx +450 -0
  266. package/src/ui/components/screens.tsx +283 -0
  267. package/src/ui/context/kwik-id-provider.tsx +72 -0
  268. package/src/ui/hooks/use-kwik-verification.ts +121 -0
  269. package/src/ui/index.ts +9 -0
@@ -0,0 +1,450 @@
1
+ /**
2
+ * KwikIDVerificationModal — the full Phase 1 verification flow.
3
+ *
4
+ * Owns a KwikIdRNSDK, drives the verification-machine reducer, renders the step
5
+ * screens + camera capture, runs upload/submit on the processing step, and reports
6
+ * a terminal VerificationOutcome via onComplete.
7
+ *
8
+ * Phase 1 = capture-only (manual selfie, no on-device liveness). The `liveness`
9
+ * step is a plain front-camera capture; Phase 2 replaces it with align + challenges.
10
+ */
11
+ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
12
+
13
+ import { ActivityIndicator, Modal, Pressable, StyleSheet, Text, View } from "react-native";
14
+
15
+ import { ApiError } from "../../api/api-error";
16
+ import { CameraView } from "../../camera/camera-view";
17
+ import { compressCapture } from "../../camera/photo-capture";
18
+ import { type Translate, createTranslator, getDeviceLocale } from "../../i18n";
19
+ import { KwikIdRNSDK } from "../../lib/kwik-id-rn-sdk";
20
+ import {
21
+ type MachineEvent,
22
+ type MachineState,
23
+ getStepProgress,
24
+ initialState,
25
+ reducer,
26
+ } from "../../lib/verification-machine";
27
+ import type {
28
+ CaptureType,
29
+ DocumentType,
30
+ KwikIdTheme,
31
+ Locale,
32
+ SessionInfo,
33
+ VerificationOutcome,
34
+ } from "../../types";
35
+ import {
36
+ ConfirmScreen,
37
+ DocumentTypeScreen,
38
+ IntroScreen,
39
+ PrepareScreen,
40
+ ProcessingScreen,
41
+ ResultScreen,
42
+ type Theme,
43
+ } from "./screens";
44
+
45
+ export interface KwikIDVerificationModalProps {
46
+ visible: boolean;
47
+ clientSecret: string;
48
+ baseUrl?: string;
49
+ appName?: string;
50
+ theme?: KwikIdTheme;
51
+ /** UI language; defaults to the device language ('fr' for French, else 'en'). */
52
+ locale?: Locale;
53
+ /** Restrict the selectable document types (intersected with the session config). */
54
+ documentTypes?: DocumentType[];
55
+ /** Skip the Expo Go runtime guard (tests only). */
56
+ skipRuntimeCheck?: boolean;
57
+ onSubmitted?: (data: { jobId: string }) => void;
58
+ onComplete: (outcome: VerificationOutcome) => void;
59
+ onClose?: () => void;
60
+ }
61
+
62
+ const DEFAULT_DOCS: DocumentType[] = ["passport", "id_card", "drivers_license"];
63
+
64
+ // Phase 1 has no handoff, so the reducer's options arg is unused — adapt to useReducer's 2-arg shape.
65
+ const machineReducer = (state: MachineState, event: MachineEvent): MachineState =>
66
+ reducer(state, event);
67
+
68
+ function resolveTheme(theme?: KwikIdTheme): Theme {
69
+ return {
70
+ primary: theme?.primary ?? "#1E3A8A",
71
+ background: theme?.background ?? "#ffffff",
72
+ foreground: theme?.foreground ?? "#111111",
73
+ };
74
+ }
75
+
76
+ export function KwikIDVerificationModal(props: KwikIDVerificationModalProps) {
77
+ const { visible, clientSecret, baseUrl, appName, documentTypes, skipRuntimeCheck } = props;
78
+ const theme = useMemo(() => resolveTheme(props.theme), [props.theme]);
79
+ const t = useMemo(() => createTranslator(props.locale ?? getDeviceLocale()), [props.locale]);
80
+
81
+ const sdk = useMemo(
82
+ () =>
83
+ new KwikIdRNSDK({
84
+ clientSecret,
85
+ baseUrl,
86
+ appName,
87
+ skipRuntimeCheck,
88
+ }),
89
+ [clientSecret, baseUrl, appName, skipRuntimeCheck]
90
+ );
91
+
92
+ const [state, dispatch] = useReducer(machineReducer, undefined, initialState);
93
+ const [sessionInfo, setSessionInfo] = useState<SessionInfo | null>(null);
94
+ const [initializing, setInitializing] = useState(false);
95
+ const startedRef = useRef(false);
96
+ const submittingRef = useRef(false);
97
+
98
+ const { step, context } = state;
99
+
100
+ // Initialize the SDK when the modal opens.
101
+ useEffect(() => {
102
+ if (!visible || startedRef.current) return;
103
+ startedRef.current = true;
104
+ setInitializing(true);
105
+ sdk.initialize()
106
+ .then((info) => setSessionInfo(info))
107
+ .catch((error: unknown) => {
108
+ dispatch({ t: "FAILED", error: describeError(error, t) });
109
+ })
110
+ .finally(() => setInitializing(false));
111
+ }, [visible, sdk]);
112
+
113
+ // Reset internal state when the modal closes, so the next open starts a fresh run
114
+ // (the useKwikVerification host keeps this component mounted between runs).
115
+ useEffect(() => {
116
+ if (!visible) {
117
+ startedRef.current = false;
118
+ submittingRef.current = false;
119
+ dispatch({ t: "RESET" });
120
+ setSessionInfo(null);
121
+ }
122
+ }, [visible]);
123
+
124
+ const allowedDocs = useMemo(() => {
125
+ const fromSession = sessionInfo?.config.documentTypes?.length
126
+ ? sessionInfo.config.documentTypes
127
+ : DEFAULT_DOCS;
128
+ return documentTypes?.length
129
+ ? fromSession.filter((d) => documentTypes.includes(d))
130
+ : fromSession;
131
+ }, [sessionInfo, documentTypes]);
132
+
133
+ // Upload + submit when entering the processing step.
134
+ useEffect(() => {
135
+ if (step !== "processing" || submittingRef.current) return;
136
+ submittingRef.current = true;
137
+
138
+ (async () => {
139
+ try {
140
+ const upload = async (
141
+ type: CaptureType,
142
+ asset?: {
143
+ uri: string;
144
+ name: string;
145
+ type: string;
146
+ width: number;
147
+ height: number;
148
+ }
149
+ ) => {
150
+ if (!asset) return;
151
+ dispatch({
152
+ t: "PROGRESS",
153
+ stage:
154
+ type === "document_front"
155
+ ? "document_front"
156
+ : type === "document_back"
157
+ ? "document_back"
158
+ : "selfie",
159
+ value: 0,
160
+ });
161
+ await sdk.uploadCapture(type, asset, (p) =>
162
+ dispatch({
163
+ t: "PROGRESS",
164
+ stage:
165
+ type === "document_front"
166
+ ? "document_front"
167
+ : type === "document_back"
168
+ ? "document_back"
169
+ : "selfie",
170
+ value: p,
171
+ })
172
+ );
173
+ };
174
+
175
+ await upload("document_front", context.front);
176
+ await upload("document_back", context.back);
177
+ await upload("selfie", context.selfie);
178
+
179
+ dispatch({ t: "PROGRESS", stage: "submitting", value: 80 });
180
+ const { jobId } = await sdk.submit(context.documentType!, {
181
+ liveness: context.livenessResult,
182
+ });
183
+ props.onSubmitted?.({ jobId });
184
+ dispatch({ t: "SUBMITTED", jobId });
185
+ } catch (error) {
186
+ dispatch({ t: "FAILED", error: describeError(error, t) });
187
+ }
188
+ })();
189
+ }, [step]);
190
+
191
+ // Report the terminal outcome (after the user taps Done on the result screen).
192
+ const finish = useCallback(() => {
193
+ const outcome = context.outcome ?? { status: "cancelled" as const };
194
+ sdk.destroy();
195
+ props.onComplete(outcome);
196
+ }, [context.outcome, sdk, props]);
197
+
198
+ const handleClose = useCallback(() => {
199
+ if (step === "processing") return; // don't allow closing mid-submit
200
+ sdk.destroy();
201
+ props.onClose?.();
202
+ props.onComplete({ status: "cancelled" });
203
+ }, [step, sdk, props]);
204
+
205
+ const onCaptured = useCallback(
206
+ async (type: CaptureType, raw: { path: string; width?: number; height?: number }) => {
207
+ // Errors propagate to CameraView, which shows them and lets the user retry.
208
+ const asset = await compressCapture(type, raw, Date.now());
209
+ if (type === "document_front") dispatch({ t: "CAPTURED", slot: "front", asset });
210
+ else if (type === "document_back") dispatch({ t: "CAPTURED", slot: "back", asset });
211
+ // No on-device liveness yet (Phase 2), so no liveness result is claimed.
212
+ else dispatch({ t: "LIVENESS_DONE", selfie: asset });
213
+ },
214
+ []
215
+ );
216
+
217
+ const canGoBack =
218
+ !["intro", "processing", "result", "liveness"].includes(step) && step !== "document-type";
219
+ const showProgress = !["intro", "processing", "result"].includes(step);
220
+
221
+ return (
222
+ <Modal
223
+ visible={visible}
224
+ animationType="slide"
225
+ onRequestClose={handleClose}
226
+ statusBarTranslucent
227
+ >
228
+ <View style={[styles.container, { backgroundColor: theme.background }]}>
229
+ {/* Header */}
230
+ <View style={styles.header}>
231
+ <Pressable
232
+ style={styles.headerBtn}
233
+ onPress={canGoBack ? () => dispatch({ t: "BACK" }) : undefined}
234
+ hitSlop={8}
235
+ >
236
+ {canGoBack ? (
237
+ <Text style={[styles.headerIcon, { color: theme.foreground }]}>‹</Text>
238
+ ) : null}
239
+ </Pressable>
240
+ <Text
241
+ style={[styles.headerTitle, { color: theme.foreground }]}
242
+ numberOfLines={1}
243
+ >
244
+ {appName ?? sessionInfo?.orgName ?? t("common.identityVerification")}
245
+ </Text>
246
+ <Pressable
247
+ style={styles.headerBtn}
248
+ onPress={step === "processing" ? undefined : handleClose}
249
+ hitSlop={8}
250
+ >
251
+ {step !== "processing" ? (
252
+ <Text style={[styles.headerIcon, { color: theme.foreground }]}>✕</Text>
253
+ ) : null}
254
+ </Pressable>
255
+ </View>
256
+ {showProgress ? (
257
+ <View style={styles.progressTrack}>
258
+ <View
259
+ style={[
260
+ styles.progressFill,
261
+ {
262
+ width: `${getStepProgress(state)}%`,
263
+ backgroundColor: theme.primary,
264
+ },
265
+ ]}
266
+ />
267
+ </View>
268
+ ) : null}
269
+
270
+ {/* Body */}
271
+ <View style={styles.body}>
272
+ {initializing && step === "intro" ? (
273
+ <View style={styles.center}>
274
+ <ActivityIndicator size="large" color={theme.primary} />
275
+ </View>
276
+ ) : null}
277
+
278
+ {step === "intro" && !initializing && (
279
+ <IntroScreen
280
+ orgName={sessionInfo?.orgName}
281
+ onNext={() => dispatch({ t: "NEXT" })}
282
+ theme={theme}
283
+ t={t}
284
+ />
285
+ )}
286
+
287
+ {step === "document-type" && (
288
+ <DocumentTypeScreen
289
+ allowed={allowedDocs}
290
+ onSelect={(doc) => dispatch({ t: "SELECT_DOC", doc })}
291
+ theme={theme}
292
+ t={t}
293
+ />
294
+ )}
295
+
296
+ {step === "prepare-front" && (
297
+ <PrepareScreen
298
+ kind="front"
299
+ onNext={() => dispatch({ t: "NEXT" })}
300
+ theme={theme}
301
+ t={t}
302
+ />
303
+ )}
304
+ {step === "prepare-back" && (
305
+ <PrepareScreen
306
+ kind="back"
307
+ onNext={() => dispatch({ t: "NEXT" })}
308
+ theme={theme}
309
+ t={t}
310
+ />
311
+ )}
312
+ {step === "prepare-selfie" && (
313
+ <PrepareScreen
314
+ kind="selfie"
315
+ onNext={() => dispatch({ t: "NEXT" })}
316
+ theme={theme}
317
+ t={t}
318
+ />
319
+ )}
320
+
321
+ {step === "camera-front" && (
322
+ <CameraView
323
+ facing="back"
324
+ active={visible}
325
+ primaryColor={theme.primary}
326
+ captureErrorText={t("camera.captureFailedMobile")}
327
+ permissionTitle={t("camera.permissionTitleMobile")}
328
+ permissionBody={t("camera.permissionBodyMobile")}
329
+ openSettingsLabel={t("camera.openSettingsMobile")}
330
+ captureLabel={t("common.capturePhoto")}
331
+ instruction={t("camera.instructionFrontMobile")}
332
+ onCapture={(raw) => onCaptured("document_front", raw)}
333
+ />
334
+ )}
335
+ {step === "camera-back" && (
336
+ <CameraView
337
+ facing="back"
338
+ active={visible}
339
+ primaryColor={theme.primary}
340
+ captureErrorText={t("camera.captureFailedMobile")}
341
+ permissionTitle={t("camera.permissionTitleMobile")}
342
+ permissionBody={t("camera.permissionBodyMobile")}
343
+ openSettingsLabel={t("camera.openSettingsMobile")}
344
+ captureLabel={t("common.capturePhoto")}
345
+ instruction={t("camera.instructionBackMobile")}
346
+ onCapture={(raw) => onCaptured("document_back", raw)}
347
+ />
348
+ )}
349
+ {step === "liveness" && (
350
+ <CameraView
351
+ facing="front"
352
+ active={visible}
353
+ primaryColor={theme.primary}
354
+ captureErrorText={t("camera.captureFailedMobile")}
355
+ permissionTitle={t("camera.permissionTitleMobile")}
356
+ permissionBody={t("camera.permissionBodyMobile")}
357
+ openSettingsLabel={t("camera.openSettingsMobile")}
358
+ captureLabel={t("common.capturePhoto")}
359
+ instruction={t("camera.instructionSelfieMobile")}
360
+ onCapture={(raw) => onCaptured("selfie", raw)}
361
+ />
362
+ )}
363
+
364
+ {step === "confirm-front" && context.front && (
365
+ <ConfirmScreen
366
+ asset={context.front}
367
+ onConfirm={() => dispatch({ t: "NEXT" })}
368
+ onRetake={() => dispatch({ t: "RETAKE", slot: "front" })}
369
+ theme={theme}
370
+ t={t}
371
+ />
372
+ )}
373
+ {step === "confirm-back" && context.back && (
374
+ <ConfirmScreen
375
+ asset={context.back}
376
+ onConfirm={() => dispatch({ t: "NEXT" })}
377
+ onRetake={() => dispatch({ t: "RETAKE", slot: "back" })}
378
+ theme={theme}
379
+ t={t}
380
+ />
381
+ )}
382
+ {step === "confirm-selfie" && context.selfie && (
383
+ <ConfirmScreen
384
+ asset={context.selfie}
385
+ onConfirm={() => dispatch({ t: "NEXT" })}
386
+ onRetake={() => dispatch({ t: "RETAKE", slot: "selfie" })}
387
+ theme={theme}
388
+ t={t}
389
+ />
390
+ )}
391
+
392
+ {step === "processing" && (
393
+ <ProcessingScreen
394
+ progress={context.uploadProgress}
395
+ stage={context.uploadStage}
396
+ theme={theme}
397
+ t={t}
398
+ />
399
+ )}
400
+
401
+ {step === "result" && (
402
+ <ResultScreen
403
+ success={context.outcome?.status === "submitted"}
404
+ message={
405
+ context.outcome?.status === "submitted"
406
+ ? t("result.submittedMessageMobile")
407
+ : (context.error ?? t("result.couldNotComplete"))
408
+ }
409
+ onDone={finish}
410
+ theme={theme}
411
+ t={t}
412
+ />
413
+ )}
414
+ </View>
415
+ </View>
416
+ </Modal>
417
+ );
418
+ }
419
+
420
+ function describeError(error: unknown, t: Translate): string {
421
+ if (error instanceof ApiError && error.isAuthError()) {
422
+ return t("errors.sessionExpiredShort");
423
+ }
424
+ return error instanceof Error ? error.message : t("errors.unexpected");
425
+ }
426
+
427
+ const styles = StyleSheet.create({
428
+ container: { flex: 1 },
429
+ header: {
430
+ flexDirection: "row",
431
+ alignItems: "center",
432
+ justifyContent: "space-between",
433
+ paddingHorizontal: 12,
434
+ paddingTop: 52,
435
+ paddingBottom: 8,
436
+ },
437
+ headerBtn: { width: 40, height: 40, alignItems: "center", justifyContent: "center" },
438
+ headerIcon: { fontSize: 24, fontWeight: "500" },
439
+ headerTitle: { flex: 1, textAlign: "center", fontSize: 16, fontWeight: "600" },
440
+ progressTrack: {
441
+ height: 3,
442
+ backgroundColor: "#e5e7eb",
443
+ marginHorizontal: 12,
444
+ borderRadius: 2,
445
+ overflow: "hidden",
446
+ },
447
+ progressFill: { height: 3 },
448
+ body: { flex: 1 },
449
+ center: { flex: 1, alignItems: "center", justifyContent: "center" },
450
+ });