@incodetech/web 2.0.0-alpha.2 → 2.0.0-alpha.3

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 (256) hide show
  1. package/dist/asset-manifest.json +16 -16
  2. package/package.json +5 -2
  3. package/.turbo/turbo-build.log +0 -58
  4. package/.turbo/turbo-coverage.log +0 -23
  5. package/.turbo/turbo-format.log +0 -6
  6. package/.turbo/turbo-lint$colon$fix.log +0 -6
  7. package/.turbo/turbo-lint.log +0 -6
  8. package/.turbo/turbo-test.log +0 -1033
  9. package/.turbo/turbo-typecheck.log +0 -5
  10. package/coverage/base.css +0 -224
  11. package/coverage/block-navigation.js +0 -87
  12. package/coverage/email/email.tsx.html +0 -850
  13. package/coverage/email/emailInput.tsx.html +0 -340
  14. package/coverage/email/index.html +0 -131
  15. package/coverage/favicon.png +0 -0
  16. package/coverage/flow/flow.tsx.html +0 -961
  17. package/coverage/flow/flowCompleted.tsx.html +0 -448
  18. package/coverage/flow/flowInit.ts.html +0 -367
  19. package/coverage/flow/flowStart.tsx.html +0 -208
  20. package/coverage/flow/index.html +0 -221
  21. package/coverage/flow/preloadFlow.ts.html +0 -598
  22. package/coverage/flow/unsupportedModule.tsx.html +0 -202
  23. package/coverage/flow/useFlowInitialization.ts.html +0 -469
  24. package/coverage/flow/useModuleLoader.ts.html +0 -361
  25. package/coverage/hooks/index.html +0 -116
  26. package/coverage/hooks/useManager.ts.html +0 -205
  27. package/coverage/index.html +0 -401
  28. package/coverage/permissions/boldWithArrow.tsx.html +0 -208
  29. package/coverage/permissions/denied.tsx.html +0 -172
  30. package/coverage/permissions/deniedAndroid.tsx.html +0 -253
  31. package/coverage/permissions/deniedDesktop.tsx.html +0 -277
  32. package/coverage/permissions/deniedIOS.tsx.html +0 -304
  33. package/coverage/permissions/deniedInstructions.tsx.html +0 -142
  34. package/coverage/permissions/iconWrapper.tsx.html +0 -130
  35. package/coverage/permissions/index.html +0 -251
  36. package/coverage/permissions/learnMore.tsx.html +0 -340
  37. package/coverage/permissions/numberedStep.tsx.html +0 -127
  38. package/coverage/permissions/permissions.tsx.html +0 -289
  39. package/coverage/phone/index.html +0 -116
  40. package/coverage/phone/phoneInput.tsx.html +0 -832
  41. package/coverage/prettify.css +0 -1
  42. package/coverage/prettify.js +0 -2
  43. package/coverage/selfie/index.html +0 -131
  44. package/coverage/selfie/selfie.tsx.html +0 -334
  45. package/coverage/selfie/tutorial.tsx.html +0 -214
  46. package/coverage/shared/baseTutorial/baseTutorial.tsx.html +0 -250
  47. package/coverage/shared/baseTutorial/index.html +0 -131
  48. package/coverage/shared/baseTutorial/replaceBaseTutorial.ts.html +0 -289
  49. package/coverage/shared/button/button.tsx.html +0 -226
  50. package/coverage/shared/button/index.html +0 -116
  51. package/coverage/shared/componentRoot/incodeComponent.tsx.html +0 -121
  52. package/coverage/shared/componentRoot/index.html +0 -116
  53. package/coverage/shared/countries/countries.ts.html +0 -502
  54. package/coverage/shared/countries/index.html +0 -116
  55. package/coverage/shared/icons/chevronDown.tsx.html +0 -151
  56. package/coverage/shared/icons/index.html +0 -131
  57. package/coverage/shared/icons/successIcon.tsx.html +0 -163
  58. package/coverage/shared/loader/index.html +0 -116
  59. package/coverage/shared/loader/loadingIcon.tsx.html +0 -286
  60. package/coverage/shared/otpInput/index.html +0 -116
  61. package/coverage/shared/otpInput/otpInput.tsx.html +0 -808
  62. package/coverage/shared/page/index.html +0 -146
  63. package/coverage/shared/page/page.tsx.html +0 -358
  64. package/coverage/shared/page/pageUiConfig.ts.html +0 -277
  65. package/coverage/shared/page/verifiedByIncode.tsx.html +0 -310
  66. package/coverage/shared/spacer/index.html +0 -116
  67. package/coverage/shared/spacer/spacer.tsx.html +0 -349
  68. package/coverage/shared/spinner/index.html +0 -116
  69. package/coverage/shared/spinner/spinner.tsx.html +0 -280
  70. package/coverage/shared/title/index.html +0 -116
  71. package/coverage/shared/title/title.tsx.html +0 -121
  72. package/coverage/shared/uiConfig/index.html +0 -116
  73. package/coverage/shared/uiConfig/uiConfig.ts.html +0 -193
  74. package/coverage/shared/webComponent/incodeModule.ts.html +0 -172
  75. package/coverage/shared/webComponent/index.html +0 -131
  76. package/coverage/shared/webComponent/registerIncodeElement.ts.html +0 -130
  77. package/coverage/sort-arrow-sprite.png +0 -0
  78. package/coverage/sorter.js +0 -210
  79. package/coverage/styles/cn.tsx.html +0 -148
  80. package/coverage/styles/fetchTheme.ts.html +0 -349
  81. package/coverage/styles/index.html +0 -131
  82. package/dev/README.md +0 -163
  83. package/dev/getToken.ts +0 -36
  84. package/dev/headless.html +0 -875
  85. package/dev/index.html +0 -366
  86. package/dev/main-headless.tsx +0 -1332
  87. package/dev/main-orchestrated-flow.tsx +0 -1158
  88. package/dev/main-preact.tsx +0 -323
  89. package/dev/main-simplified.tsx +0 -123
  90. package/dev/main-web-component.tsx +0 -256
  91. package/dev/main.tsx +0 -332
  92. package/dev/manual.html +0 -27
  93. package/dev/orchestrated-flow.html +0 -64
  94. package/dev/simplified.html +0 -64
  95. package/dev/tiktok-logo.svg +0 -7
  96. package/src/defineCustomElement.tsx +0 -30
  97. package/src/email/email.test.tsx +0 -368
  98. package/src/email/email.tsx +0 -255
  99. package/src/email/emailInput.test.tsx +0 -264
  100. package/src/email/emailInput.tsx +0 -85
  101. package/src/email/styles.css +0 -59
  102. package/src/flow/flow.test.tsx +0 -796
  103. package/src/flow/flow.tsx +0 -292
  104. package/src/flow/flowCompleted.css +0 -30
  105. package/src/flow/flowCompleted.test.tsx +0 -331
  106. package/src/flow/flowCompleted.tsx +0 -121
  107. package/src/flow/flowInit.test.ts +0 -264
  108. package/src/flow/flowInit.ts +0 -94
  109. package/src/flow/flowStart.css +0 -58
  110. package/src/flow/flowStart.test.tsx +0 -49
  111. package/src/flow/flowStart.tsx +0 -41
  112. package/src/flow/incode-logo.svg +0 -8
  113. package/src/flow/index.ts +0 -7
  114. package/src/flow/preloadFlow.test.ts +0 -421
  115. package/src/flow/preloadFlow.ts +0 -171
  116. package/src/flow/styles.css +0 -9
  117. package/src/flow/unsupportedModule.css +0 -21
  118. package/src/flow/unsupportedModule.tsx +0 -39
  119. package/src/flow/useFlowInitialization.test.tsx +0 -292
  120. package/src/flow/useFlowInitialization.ts +0 -128
  121. package/src/flow/useModuleLoader.test.tsx +0 -212
  122. package/src/flow/useModuleLoader.ts +0 -92
  123. package/src/hooks/index.ts +0 -1
  124. package/src/hooks/useManager.test.ts +0 -91
  125. package/src/hooks/useManager.ts +0 -40
  126. package/src/i18n/index.ts +0 -3
  127. package/src/i18n/instance.ts +0 -16
  128. package/src/i18n/setup.ts +0 -184
  129. package/src/i18n/useTranslation.ts +0 -42
  130. package/src/index.ts +0 -27
  131. package/src/permissions/assets/android-dots-icon.svg +0 -7
  132. package/src/permissions/assets/android-settings-icon.svg +0 -16
  133. package/src/permissions/assets/android-toggle-icon.svg +0 -20
  134. package/src/permissions/assets/bank-card-icon.svg +0 -14
  135. package/src/permissions/assets/camera-icon.svg +0 -12
  136. package/src/permissions/assets/camera-ios.svg +0 -53
  137. package/src/permissions/assets/check-icon.svg +0 -8
  138. package/src/permissions/assets/chrome-icon.svg +0 -43
  139. package/src/permissions/assets/password-icon.svg +0 -11
  140. package/src/permissions/assets/permissions-img.svg +0 -51
  141. package/src/permissions/assets/safari-icon.svg +0 -37
  142. package/src/permissions/assets/settings-icon.svg +0 -33
  143. package/src/permissions/assets/toggle-icon.svg +0 -19
  144. package/src/permissions/assets/warning-icon.svg +0 -6
  145. package/src/permissions/boldWithArrow.css +0 -9
  146. package/src/permissions/boldWithArrow.tsx +0 -41
  147. package/src/permissions/denied.css +0 -37
  148. package/src/permissions/denied.tsx +0 -29
  149. package/src/permissions/deniedAndroid.tsx +0 -56
  150. package/src/permissions/deniedDesktop.css +0 -9
  151. package/src/permissions/deniedDesktop.tsx +0 -64
  152. package/src/permissions/deniedIOS.tsx +0 -73
  153. package/src/permissions/deniedInstructions.tsx +0 -19
  154. package/src/permissions/iconWrapper.css +0 -9
  155. package/src/permissions/iconWrapper.tsx +0 -15
  156. package/src/permissions/learnMore.css +0 -37
  157. package/src/permissions/learnMore.tsx +0 -85
  158. package/src/permissions/numberedStep.css +0 -13
  159. package/src/permissions/numberedStep.tsx +0 -14
  160. package/src/permissions/permissions.css +0 -13
  161. package/src/permissions/permissions.tsx +0 -68
  162. package/src/phone/phone.tsx +0 -246
  163. package/src/phone/phoneInput.test.tsx +0 -275
  164. package/src/phone/phoneInput.tsx +0 -249
  165. package/src/phone/styles.css +0 -158
  166. package/src/selfie/cameraButton.css +0 -13
  167. package/src/selfie/cameraButton.tsx +0 -35
  168. package/src/selfie/capture.css +0 -57
  169. package/src/selfie/capture.tsx +0 -232
  170. package/src/selfie/errorModal.tsx +0 -218
  171. package/src/selfie/errorModalContent.css +0 -33
  172. package/src/selfie/errorModalContent.tsx +0 -44
  173. package/src/selfie/faceOutline.css +0 -5
  174. package/src/selfie/faceOutline.tsx +0 -22
  175. package/src/selfie/loadingBorder.css +0 -12
  176. package/src/selfie/loadingBorder.tsx +0 -77
  177. package/src/selfie/manualCaptureButton.css +0 -13
  178. package/src/selfie/manualCaptureButton.tsx +0 -35
  179. package/src/selfie/noMoreAttemptsModal.tsx +0 -44
  180. package/src/selfie/notification.css +0 -9
  181. package/src/selfie/notification.tsx +0 -36
  182. package/src/selfie/retryErrorModal.tsx +0 -56
  183. package/src/selfie/selfie.test.tsx +0 -458
  184. package/src/selfie/selfie.tsx +0 -83
  185. package/src/selfie/selfieTutorial.json +0 -2626
  186. package/src/selfie/styles.css +0 -1
  187. package/src/selfie/tutorial.test.tsx +0 -200
  188. package/src/selfie/tutorial.tsx +0 -43
  189. package/src/setup.ts +0 -33
  190. package/src/shared/baseTutorial/baseTutorial.css +0 -21
  191. package/src/shared/baseTutorial/baseTutorial.test.tsx +0 -184
  192. package/src/shared/baseTutorial/baseTutorial.tsx +0 -55
  193. package/src/shared/baseTutorial/replaceBaseTutorial.test.ts +0 -267
  194. package/src/shared/baseTutorial/replaceBaseTutorial.ts +0 -68
  195. package/src/shared/button/button.css +0 -55
  196. package/src/shared/button/button.test.tsx +0 -101
  197. package/src/shared/button/button.tsx +0 -47
  198. package/src/shared/componentRoot/incodeComponent.tsx +0 -12
  199. package/src/shared/countries/countries.test.ts +0 -75
  200. package/src/shared/countries/countries.ts +0 -139
  201. package/src/shared/countries/index.ts +0 -6
  202. package/src/shared/icons/chevronDown.tsx +0 -22
  203. package/src/shared/icons/index.ts +0 -2
  204. package/src/shared/icons/successIcon.css +0 -5
  205. package/src/shared/icons/successIcon.test.tsx +0 -40
  206. package/src/shared/icons/successIcon.tsx +0 -26
  207. package/src/shared/loader/loadingIcon.css +0 -28
  208. package/src/shared/loader/loadingIcon.tsx +0 -67
  209. package/src/shared/lottie/lottie.tsx +0 -108
  210. package/src/shared/otpInput/otpInput.css +0 -85
  211. package/src/shared/otpInput/otpInput.test.tsx +0 -356
  212. package/src/shared/otpInput/otpInput.tsx +0 -241
  213. package/src/shared/page/incode-logo.svg +0 -3
  214. package/src/shared/page/page.css +0 -47
  215. package/src/shared/page/page.test.tsx +0 -97
  216. package/src/shared/page/page.tsx +0 -91
  217. package/src/shared/page/pageUiConfig.test.ts +0 -112
  218. package/src/shared/page/pageUiConfig.ts +0 -64
  219. package/src/shared/page/verifiedByIncode.css +0 -5
  220. package/src/shared/page/verifiedByIncode.tsx +0 -75
  221. package/src/shared/spacer/spacer.css +0 -149
  222. package/src/shared/spacer/spacer.test.tsx +0 -143
  223. package/src/shared/spacer/spacer.tsx +0 -88
  224. package/src/shared/spinner/index.ts +0 -2
  225. package/src/shared/spinner/spinner.css +0 -28
  226. package/src/shared/spinner/spinner.test.tsx +0 -82
  227. package/src/shared/spinner/spinner.tsx +0 -65
  228. package/src/shared/title/title.css +0 -7
  229. package/src/shared/title/title.tsx +0 -12
  230. package/src/shared/uiConfig/uiConfig.ts +0 -36
  231. package/src/shared/webComponent/incodeModule.ts +0 -29
  232. package/src/shared/webComponent/registerIncodeElement.ts +0 -15
  233. package/src/styles/__mocks__/fetchTheme.ts +0 -19
  234. package/src/styles/applyTheme.ts +0 -37
  235. package/src/styles/cn.test.tsx +0 -57
  236. package/src/styles/cn.tsx +0 -21
  237. package/src/styles/core.css +0 -12
  238. package/src/styles/fetchTheme.test.ts +0 -390
  239. package/src/styles/fetchTheme.ts +0 -88
  240. package/src/styles/generatePalette.ts +0 -111
  241. package/src/styles/reset.css +0 -65
  242. package/src/styles/resolveCssVariableToHex.ts +0 -28
  243. package/src/styles/tailwind.css +0 -291
  244. package/src/styles/themeTypes.ts +0 -18
  245. package/src/styles/tokens/colors.css +0 -190
  246. package/src/styles/tokens/components.css +0 -174
  247. package/src/styles/tokens/index.css +0 -4
  248. package/src/styles/tokens/primitives.css +0 -129
  249. package/src/styles/tokens/semantic.css +0 -51
  250. package/src/svg.d.ts +0 -4
  251. package/src/types/assets.d.ts +0 -1
  252. package/src/types/custom-elements.d.ts +0 -104
  253. package/tsconfig.json +0 -22
  254. package/vite.config.ts +0 -260
  255. package/vitest.config.ts +0 -40
  256. package/vitest.setup.ts +0 -16
@@ -1,1332 +0,0 @@
1
- /**
2
- * Headless SDK Demo
3
- *
4
- * This demonstrates using the Incode Core SDK without ANY pre-built UI components.
5
- * The left panel shows YOUR CUSTOM UI - built however you want.
6
- * The right panel shows the SDK STATE - the logic engine driving everything.
7
- *
8
- * Flow:
9
- * 1. User clicks "Start Session" → Creates session via API
10
- * 2. Flow config is loaded → Determines module order (e.g., Phone → Email → Selfie)
11
- * 3. Each module renders with YOUR custom UI
12
- * 4. SDK state machine handles all business logic
13
- */
14
-
15
- import type { WasmConfig } from '@incodetech/core';
16
- import { createSession, setup } from '@incodetech/core';
17
- import {
18
- createEmailManager,
19
- type EmailConfig,
20
- type EmailState,
21
- emailMachine,
22
- } from '@incodetech/core/email';
23
- import {
24
- createOrchestratedFlowManager,
25
- type FlowModuleConfig,
26
- type OrchestratedFlowState,
27
- } from '@incodetech/core/flow';
28
- import {
29
- createPhoneManager,
30
- type PhoneState,
31
- phoneMachine,
32
- } from '@incodetech/core/phone';
33
- import {
34
- createSelfieManager,
35
- type SelfieState,
36
- selfieMachine,
37
- } from '@incodetech/core/selfie';
38
- import type { JSX } from 'preact';
39
- import { render } from 'preact';
40
- import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
41
- import { getConfigIdFromUrl } from './getToken';
42
-
43
- const API_URL = 'https://user-service-k8s.stage.incodetest.com/0';
44
- const API_KEY = '5fbc0ab5a652b7808fbec42fffd8b4c9ec248413';
45
-
46
- const WASM_CONFIG: WasmConfig = {
47
- wasmPath:
48
- 'https://d3vv997wtl2myz.cloudfront.net/webcamera/onnx-backend-wasm/v2.12.47/webLib.wasm',
49
- wasmSimdPath:
50
- 'https://d3vv997wtl2myz.cloudfront.net/webcamera/onnx-backend-wasm/v2.12.47/webLibSimd.wasm',
51
- glueCodePath:
52
- 'https://d3vv997wtl2myz.cloudfront.net/webcamera/onnx-backend-wasm/v2.12.47/webLib.js',
53
- modelsBasePath:
54
- 'https://d3vv997wtl2myz.cloudfront.net/webcamera/onnx-backend-wasm/v2.12.47/models',
55
- pipelines: ['selfie'],
56
- };
57
-
58
- // ============================================================================
59
- // Types
60
- // ============================================================================
61
-
62
- type LogEntry = {
63
- time: string;
64
- type: 'action' | 'state' | 'init' | 'error' | 'flow';
65
- message: string;
66
- };
67
-
68
- type FlowManager = ReturnType<typeof createOrchestratedFlowManager>;
69
- type PhoneManager = ReturnType<typeof createPhoneManager>;
70
- type EmailManager = ReturnType<typeof createEmailManager>;
71
- type SelfieManager = ReturnType<typeof createSelfieManager>;
72
-
73
- // ============================================================================
74
- // JSON Syntax Highlighter
75
- // ============================================================================
76
-
77
- function highlightJSON(obj: unknown, indent = 0): JSX.Element[] {
78
- const spaces = ' '.repeat(indent);
79
- const elements: JSX.Element[] = [];
80
-
81
- if (obj === null) {
82
- elements.push(<span class="json-null">null</span>);
83
- } else if (typeof obj === 'boolean') {
84
- elements.push(<span class="json-boolean">{String(obj)}</span>);
85
- } else if (typeof obj === 'number') {
86
- elements.push(<span class="json-number">{obj}</span>);
87
- } else if (typeof obj === 'string') {
88
- const display = obj.length > 50 ? `${obj.slice(0, 50)}...` : obj;
89
- elements.push(<span class="json-string">"{display}"</span>);
90
- } else if (Array.isArray(obj)) {
91
- if (obj.length === 0) {
92
- elements.push(<span class="json-bracket">[]</span>);
93
- } else {
94
- elements.push(<span class="json-bracket">[{'\n'}</span>);
95
- obj.forEach((item, i) => {
96
- elements.push(<span>{spaces} </span>);
97
- elements.push(...highlightJSON(item, indent + 1));
98
- if (i < obj.length - 1) elements.push(<span>,</span>);
99
- elements.push(<span>{'\n'}</span>);
100
- });
101
- elements.push(<span>{spaces}</span>);
102
- elements.push(<span class="json-bracket">]</span>);
103
- }
104
- } else if (typeof obj === 'object') {
105
- const filteredObj: Record<string, unknown> = {};
106
- for (const [key, value] of Object.entries(obj)) {
107
- if (value instanceof MediaStream) {
108
- filteredObj[key] = '[MediaStream]';
109
- } else if (value instanceof HTMLCanvasElement) {
110
- filteredObj[key] = '[Canvas]';
111
- } else {
112
- filteredObj[key] = value;
113
- }
114
- }
115
- const entries = Object.entries(filteredObj);
116
- if (entries.length === 0) {
117
- elements.push(<span class="json-bracket">{'{}'}</span>);
118
- } else {
119
- elements.push(<span class="json-bracket">{'{\n'}</span>);
120
- entries.forEach(([key, value], i) => {
121
- elements.push(<span>{spaces} </span>);
122
- elements.push(<span class="json-key">"{key}"</span>);
123
- elements.push(<span class="json-bracket">: </span>);
124
- elements.push(...highlightJSON(value, indent + 1));
125
- if (i < entries.length - 1) elements.push(<span>,</span>);
126
- elements.push(<span>{'\n'}</span>);
127
- });
128
- elements.push(<span>{spaces}</span>);
129
- elements.push(<span class="json-bracket">{'}'}</span>);
130
- }
131
- }
132
-
133
- return elements;
134
- }
135
-
136
- // ============================================================================
137
- // SDK Panel Components
138
- // ============================================================================
139
-
140
- function StateViewer({ state, label }: { state: unknown; label: string }) {
141
- return (
142
- <div class="state-viewer">
143
- <div class="state-viewer-header">
144
- <span>{label}</span>
145
- </div>
146
- <div class="state-viewer-content">
147
- <pre>{highlightJSON(state)}</pre>
148
- </div>
149
- </div>
150
- );
151
- }
152
-
153
- function EventLog({ entries }: { entries: LogEntry[] }) {
154
- return (
155
- <div class="event-log">
156
- <div class="event-log-title">Event Stream</div>
157
- <div class="event-log-content">
158
- {entries.slice(0, 50).map((entry, i) => (
159
- <div key={i} class="log-entry">
160
- <span class="log-time">{entry.time}</span>
161
- <span class={`log-event log-${entry.type}`}>
162
- [{entry.type.toUpperCase()}]
163
- </span>
164
- <span class="log-data"> {entry.message}</span>
165
- </div>
166
- ))}
167
- </div>
168
- </div>
169
- );
170
- }
171
-
172
- function FlowProgress({
173
- steps,
174
- currentIndex,
175
- }: {
176
- steps: string[];
177
- currentIndex: number;
178
- }) {
179
- return (
180
- <div class="flow-progress">
181
- {steps.map((step, i) => (
182
- <div
183
- key={step}
184
- class={`flow-step ${i < currentIndex ? 'done' : ''} ${i === currentIndex ? 'active' : ''}`}
185
- >
186
- <span class="step-number">{i + 1}</span>
187
- <span class="step-name">{step}</span>
188
- </div>
189
- ))}
190
- </div>
191
- );
192
- }
193
-
194
- // ============================================================================
195
- // Welcome / Session Start UI
196
- // ============================================================================
197
-
198
- function WelcomeUI({
199
- onStartSession,
200
- isStarting,
201
- configId,
202
- onConfigIdChange,
203
- }: {
204
- onStartSession: () => void;
205
- isStarting: boolean;
206
- configId: string;
207
- onConfigIdChange: (id: string) => void;
208
- }) {
209
- return (
210
- <div class="welcome-container">
211
- <div class="welcome-icon">🔐</div>
212
- <h1 class="welcome-title">Headless SDK Demo</h1>
213
- <p class="welcome-description">
214
- Build your own UI while the SDK handles all business logic, API calls,
215
- and state management.
216
- </p>
217
-
218
- <div class="config-input-group">
219
- <label class="config-label">
220
- Configuration ID
221
- <input
222
- type="text"
223
- class="config-input"
224
- value={configId}
225
- onInput={(e) =>
226
- onConfigIdChange((e.target as HTMLInputElement).value)
227
- }
228
- placeholder="Enter your flow configuration ID"
229
- />
230
- </label>
231
- </div>
232
-
233
- <div class="feature-list">
234
- <div class="feature">
235
- <span class="feature-icon">📱</span>
236
- <span>Phone verification with OTP</span>
237
- </div>
238
- <div class="feature">
239
- <span class="feature-icon">✉️</span>
240
- <span>Email verification</span>
241
- </div>
242
- <div class="feature">
243
- <span class="feature-icon">📸</span>
244
- <span>Selfie capture with WASM face detection</span>
245
- </div>
246
- </div>
247
-
248
- <button
249
- type="button"
250
- class="btn btn-primary btn-large"
251
- onClick={onStartSession}
252
- disabled={isStarting || !configId.trim()}
253
- >
254
- {isStarting ? (
255
- <>
256
- <span class="spinner-small" /> Creating Session...
257
- </>
258
- ) : (
259
- 'Start Session →'
260
- )}
261
- </button>
262
-
263
- <div class="code-comment">
264
- {`// Step 1: Create session
265
- const session = await createSession(apiKey, { configurationId });
266
-
267
- // Step 2: Setup SDK with token
268
- await setup({ apiURL, token: session.token, wasm: wasmConfig });
269
-
270
- // Step 3: Load flow to get module order
271
- flowManager.load();`}
272
- </div>
273
- </div>
274
- );
275
- }
276
-
277
- // ============================================================================
278
- // Phone UI Components
279
- // ============================================================================
280
-
281
- function PhoneInputUI({
282
- state,
283
- manager,
284
- log,
285
- }: {
286
- state: PhoneState & { status: 'inputting' };
287
- manager: PhoneManager;
288
- log: (type: LogEntry['type'], msg: string) => void;
289
- }) {
290
- const [phone, setPhone] = useState(state.prefilledPhone || '');
291
- const [isValid, setIsValid] = useState(false);
292
- const inputRef = useRef<HTMLInputElement>(null);
293
-
294
- useEffect(() => {
295
- inputRef.current?.focus();
296
- }, []);
297
-
298
- const handleInput = (e: Event) => {
299
- const value = (e.target as HTMLInputElement).value;
300
- setPhone(value);
301
- const valid = /^\+\d{10,}$/.test(value.replace(/\s/g, ''));
302
- setIsValid(valid);
303
- log('action', `setPhoneNumber("${value}", ${valid})`);
304
- manager.setPhoneNumber(value, valid);
305
- };
306
-
307
- return (
308
- <div class="form-container">
309
- <div class="form-header">
310
- <div class="form-icon">📱</div>
311
- <h2 class="form-title">Enter Your Phone</h2>
312
- <p class="form-description">Include country code (e.g., +1...)</p>
313
- </div>
314
-
315
- <div class="input-group">
316
- <label class="input-label">
317
- Phone Number ({state.countryCode})
318
- <input
319
- ref={inputRef}
320
- type="tel"
321
- class="input-field"
322
- value={phone}
323
- onInput={handleInput}
324
- placeholder={`${state.phonePrefix} 555 123 4567`}
325
- />
326
- </label>
327
- <div class={`validation-status ${isValid ? 'valid' : 'invalid'}`}>
328
- {isValid ? '✓ Valid format' : '○ Enter full number with country code'}
329
- </div>
330
- </div>
331
-
332
- <button
333
- type="button"
334
- class="btn btn-primary"
335
- disabled={!isValid}
336
- onClick={() => {
337
- log('action', 'submit()');
338
- manager.submit();
339
- }}
340
- >
341
- Send Verification Code
342
- </button>
343
- </div>
344
- );
345
- }
346
-
347
- function PhoneOtpUI({
348
- state,
349
- manager,
350
- log,
351
- }: {
352
- state: PhoneState & { status: 'awaitingOtp' };
353
- manager: PhoneManager;
354
- log: (type: LogEntry['type'], msg: string) => void;
355
- }) {
356
- const inputRef = useRef<HTMLInputElement>(null);
357
- const [canSubmit, setCanSubmit] = useState(false);
358
-
359
- useEffect(() => {
360
- inputRef.current?.focus();
361
- }, []);
362
-
363
- const handleSubmit = () => {
364
- const otp = inputRef.current?.value ?? '';
365
- if (otp.length === 6) {
366
- log('action', `submitOtp("${otp}")`);
367
- manager.submitOtp(otp);
368
- }
369
- };
370
-
371
- return (
372
- <div class="form-container">
373
- <div class="form-header">
374
- <div class="form-icon">🔐</div>
375
- <h2 class="form-title">Enter Verification Code</h2>
376
- <p class="form-description">6-digit code sent to your phone</p>
377
- </div>
378
-
379
- <div class="input-group">
380
- <input
381
- ref={inputRef}
382
- type="text"
383
- class="input-field"
384
- style={{
385
- textAlign: 'center',
386
- fontSize: '1.5rem',
387
- letterSpacing: '0.3em',
388
- }}
389
- placeholder="ABC123"
390
- onInput={(e) => {
391
- const input = e.target as HTMLInputElement;
392
- // Allow alphanumeric, limit to 6
393
- const cleaned = input.value
394
- .replace(/[^a-zA-Z0-9]/g, '')
395
- .toUpperCase()
396
- .slice(0, 6);
397
- if (cleaned !== input.value) {
398
- input.value = cleaned;
399
- }
400
- setCanSubmit(cleaned.length === 6);
401
- }}
402
- onKeyDown={(e) => {
403
- if (e.key === 'Enter' && canSubmit) {
404
- handleSubmit();
405
- }
406
- }}
407
- maxLength={6}
408
- />
409
- </div>
410
-
411
- <button
412
- type="button"
413
- class="btn btn-primary"
414
- onClick={handleSubmit}
415
- disabled={!canSubmit}
416
- >
417
- Verify Code
418
- </button>
419
-
420
- <div class="timer">
421
- {state.canResend ? (
422
- <button
423
- type="button"
424
- class="btn btn-ghost"
425
- onClick={() => {
426
- log('action', 'resendOtp()');
427
- manager.resendOtp();
428
- }}
429
- >
430
- Resend Code
431
- </button>
432
- ) : (
433
- <span>
434
- Resend in <span class="timer-value">{state.resendTimer}s</span>
435
- </span>
436
- )}
437
- </div>
438
- </div>
439
- );
440
- }
441
-
442
- // ============================================================================
443
- // Email UI Components
444
- // ============================================================================
445
-
446
- function EmailInputUI({
447
- state,
448
- manager,
449
- log,
450
- }: {
451
- state: EmailState & { status: 'inputting' };
452
- manager: EmailManager;
453
- log: (type: LogEntry['type'], msg: string) => void;
454
- }) {
455
- const [email, setEmail] = useState(state.prefilledEmail || '');
456
- const [isValid, setIsValid] = useState(false);
457
- const inputRef = useRef<HTMLInputElement>(null);
458
-
459
- useEffect(() => {
460
- inputRef.current?.focus();
461
- }, []);
462
-
463
- const handleInput = (e: Event) => {
464
- const value = (e.target as HTMLInputElement).value;
465
- setEmail(value);
466
- const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
467
- setIsValid(valid);
468
- log('action', `setEmail("${value}", ${valid})`);
469
- manager.setEmail(value, valid);
470
- };
471
-
472
- return (
473
- <div class="form-container">
474
- <div class="form-header">
475
- <div class="form-icon">✉️</div>
476
- <h2 class="form-title">Enter Your Email</h2>
477
- <p class="form-description">We'll send a verification code</p>
478
- </div>
479
-
480
- <div class="input-group">
481
- <label class="input-label">
482
- Email Address
483
- <input
484
- ref={inputRef}
485
- type="email"
486
- class="input-field"
487
- value={email}
488
- onInput={handleInput}
489
- placeholder="you@example.com"
490
- />
491
- </label>
492
- <div class={`validation-status ${isValid ? 'valid' : 'invalid'}`}>
493
- {isValid ? '✓ Valid email' : '○ Enter a valid email'}
494
- </div>
495
- </div>
496
-
497
- <button
498
- type="button"
499
- class="btn btn-primary"
500
- disabled={!isValid}
501
- onClick={() => {
502
- log('action', 'submit()');
503
- manager.submit();
504
- }}
505
- >
506
- Send Verification Code
507
- </button>
508
- </div>
509
- );
510
- }
511
-
512
- function EmailOtpUI({
513
- state,
514
- manager,
515
- log,
516
- }: {
517
- state: EmailState & { status: 'awaitingOtp' };
518
- manager: EmailManager;
519
- log: (type: LogEntry['type'], msg: string) => void;
520
- }) {
521
- const inputRef = useRef<HTMLInputElement>(null);
522
- const [canSubmit, setCanSubmit] = useState(false);
523
-
524
- useEffect(() => {
525
- inputRef.current?.focus();
526
- }, []);
527
-
528
- const handleSubmit = () => {
529
- const otp = inputRef.current?.value ?? '';
530
- if (otp.length === 6) {
531
- log('action', `submitOtp("${otp}")`);
532
- manager.submitOtp(otp);
533
- }
534
- };
535
-
536
- return (
537
- <div class="form-container">
538
- <div class="form-header">
539
- <div class="form-icon">📧</div>
540
- <h2 class="form-title">Check Your Email</h2>
541
- <p class="form-description">Enter the 6-digit code we sent</p>
542
- </div>
543
-
544
- <div class="input-group">
545
- <input
546
- ref={inputRef}
547
- type="text"
548
- class="input-field"
549
- style={{
550
- textAlign: 'center',
551
- fontSize: '1.5rem',
552
- letterSpacing: '0.3em',
553
- }}
554
- placeholder="ABC123"
555
- onInput={(e) => {
556
- const input = e.target as HTMLInputElement;
557
- // Allow alphanumeric, limit to 6
558
- const cleaned = input.value
559
- .replace(/[^a-zA-Z0-9]/g, '')
560
- .toUpperCase()
561
- .slice(0, 6);
562
- if (cleaned !== input.value) {
563
- input.value = cleaned;
564
- }
565
- setCanSubmit(cleaned.length === 6);
566
- }}
567
- onKeyDown={(e) => {
568
- if (e.key === 'Enter' && canSubmit) {
569
- handleSubmit();
570
- }
571
- }}
572
- maxLength={6}
573
- />
574
- </div>
575
-
576
- <button
577
- type="button"
578
- class="btn btn-primary"
579
- onClick={handleSubmit}
580
- disabled={!canSubmit}
581
- >
582
- Verify Code
583
- </button>
584
-
585
- <div class="timer">
586
- {state.canResend ? (
587
- <button
588
- type="button"
589
- class="btn btn-ghost"
590
- onClick={() => {
591
- log('action', 'resendOtp()');
592
- manager.resendOtp();
593
- }}
594
- >
595
- Resend Code
596
- </button>
597
- ) : (
598
- <span>
599
- Resend in <span class="timer-value">{state.resendTimer}s</span>
600
- </span>
601
- )}
602
- </div>
603
- </div>
604
- );
605
- }
606
-
607
- // ============================================================================
608
- // Selfie UI Components
609
- // ============================================================================
610
-
611
- function SelfieTutorialUI({
612
- manager,
613
- log,
614
- }: {
615
- manager: SelfieManager;
616
- log: (type: LogEntry['type'], msg: string) => void;
617
- }) {
618
- return (
619
- <div class="form-container">
620
- <div class="form-header">
621
- <div class="form-icon">💡</div>
622
- <h2 class="form-title">Selfie Tips</h2>
623
- </div>
624
-
625
- <ul class="tips-list">
626
- <li>✓ Good lighting on your face</li>
627
- <li>✓ Remove glasses if possible</li>
628
- <li>✓ Look directly at the camera</li>
629
- <li>✓ Keep a neutral expression</li>
630
- </ul>
631
-
632
- <button
633
- type="button"
634
- class="btn btn-primary"
635
- onClick={() => {
636
- log('action', 'nextStep()');
637
- manager.nextStep();
638
- }}
639
- >
640
- Continue →
641
- </button>
642
- </div>
643
- );
644
- }
645
-
646
- function SelfiePermissionsUI({
647
- state,
648
- manager,
649
- log,
650
- }: {
651
- state: SelfieState & { status: 'permissions' };
652
- manager: SelfieManager;
653
- log: (type: LogEntry['type'], msg: string) => void;
654
- }) {
655
- const isDenied = state.permissionStatus === 'denied';
656
-
657
- return (
658
- <div class="form-container">
659
- <div class="form-header">
660
- <div class="form-icon">{isDenied ? '🚫' : '📷'}</div>
661
- <h2 class="form-title">
662
- {isDenied ? 'Camera Blocked' : 'Camera Access'}
663
- </h2>
664
- <p class="form-description">
665
- {isDenied
666
- ? 'Enable camera in browser settings'
667
- : 'We need camera access for selfie capture'}
668
- </p>
669
- </div>
670
-
671
- {!isDenied && (
672
- <button
673
- type="button"
674
- class="btn btn-primary"
675
- onClick={() => {
676
- log('action', 'requestPermission()');
677
- manager.requestPermission();
678
- }}
679
- >
680
- Allow Camera
681
- </button>
682
- )}
683
- </div>
684
- );
685
- }
686
-
687
- function SelfieCaptureUI({
688
- state,
689
- manager,
690
- log,
691
- onContinue,
692
- }: {
693
- state: SelfieState & { status: 'capture' };
694
- manager: SelfieManager;
695
- log: (type: LogEntry['type'], msg: string) => void;
696
- onContinue: () => void;
697
- }) {
698
- const videoRef = useRef<HTMLVideoElement>(null);
699
-
700
- // Attach stream to video element
701
- useEffect(() => {
702
- const video = videoRef.current;
703
- if (!video) return;
704
-
705
- if (state.stream) {
706
- log('state', `[SELFIE] Attaching stream to video`);
707
- video.srcObject = state.stream;
708
- video.play().catch((err) => {
709
- log('error', `Video play failed: ${err.message}`);
710
- });
711
- } else {
712
- log('state', `[SELFIE] No stream available yet`);
713
- }
714
- }, [state.stream, log]);
715
-
716
- const getStatusMessage = () => {
717
- switch (state.detectionStatus) {
718
- case 'noFace':
719
- return { icon: '👤', text: 'No face detected' };
720
- case 'tooClose':
721
- return { icon: '↔️', text: 'Move back a bit' };
722
- case 'tooFar':
723
- return { icon: '↔️', text: 'Move closer' };
724
- case 'blur':
725
- return { icon: '🔍', text: 'Hold steady' };
726
- case 'dark':
727
- return { icon: '💡', text: 'Need more light' };
728
- case 'faceAngle':
729
- return { icon: '🔄', text: 'Face the camera' };
730
- case 'tooManyFaces':
731
- return { icon: '👥', text: 'Only one face' };
732
- case 'centerFace':
733
- return { icon: '⭕', text: 'Center your face' };
734
- case 'getReady':
735
- return { icon: '✨', text: 'Hold still...' };
736
- case 'success':
737
- return { icon: '✅', text: 'Perfect!' };
738
- case 'manualCapture':
739
- return { icon: '📸', text: 'Tap to capture' };
740
- case 'capturing':
741
- return { icon: '📷', text: 'Capturing...' };
742
- default:
743
- return { icon: '🔍', text: 'Detecting...' };
744
- }
745
- };
746
-
747
- const status = getStatusMessage();
748
- const isManualMode = state.detectionStatus === 'manualCapture';
749
- const isSuccess =
750
- state.detectionStatus === 'success' || state.detectionStatus === 'getReady';
751
-
752
- // Show loading if no stream yet
753
- if (!state.stream) {
754
- return (
755
- <div class="capture-container">
756
- <div
757
- class="camera-wrapper"
758
- style={{
759
- display: 'flex',
760
- alignItems: 'center',
761
- justifyContent: 'center',
762
- }}
763
- >
764
- <div style={{ textAlign: 'center', color: 'var(--text-secondary)' }}>
765
- <div class="spinner" style={{ margin: '0 auto 1rem' }} />
766
- <p>Starting camera...</p>
767
- <p style={{ fontSize: '0.75rem', marginTop: '0.5rem' }}>
768
- captureStatus: {state.captureStatus}
769
- </p>
770
- </div>
771
- </div>
772
- </div>
773
- );
774
- }
775
-
776
- return (
777
- <div class="capture-container">
778
- <div class="camera-wrapper">
779
- <video ref={videoRef} autoPlay playsInline muted class="camera-feed" />
780
-
781
- <div class="face-guide">
782
- <svg viewBox="0 0 200 260" class="face-oval" aria-label="Face guide">
783
- <title>Face positioning guide</title>
784
- <ellipse
785
- cx="100"
786
- cy="130"
787
- rx="75"
788
- ry="100"
789
- fill="none"
790
- stroke={isSuccess ? '#00ff88' : '#ffffff'}
791
- stroke-width="3"
792
- stroke-dasharray={isSuccess ? 'none' : '8 4'}
793
- />
794
- </svg>
795
- </div>
796
-
797
- <div class={`capture-status ${isSuccess ? 'success' : ''}`}>
798
- <span class="status-icon">{status.icon}</span>
799
- <span class="status-text">{status.text}</span>
800
- </div>
801
- </div>
802
-
803
- {isManualMode && (
804
- <button
805
- type="button"
806
- class="btn btn-primary capture-btn"
807
- onClick={() => {
808
- log('action', 'capture()');
809
- manager.capture();
810
- }}
811
- >
812
- 📸 Capture Now
813
- </button>
814
- )}
815
-
816
- {state.captureStatus === 'uploading' && (
817
- <div class="upload-status">
818
- <span class="spinner-small" />
819
- <span>Uploading...</span>
820
- </div>
821
- )}
822
-
823
- {state.captureStatus === 'uploadError' && (
824
- <div class="upload-error">
825
- <div class="error-icon">⚠️</div>
826
- <p class="error-message">{state.uploadError ?? 'Upload failed'}</p>
827
- <p class="attempts-remaining">
828
- Attempts remaining: {state.attemptsRemaining}
829
- </p>
830
- {state.attemptsRemaining > 0 ? (
831
- <button
832
- type="button"
833
- class="btn btn-primary"
834
- onClick={() => {
835
- log('action', 'retryCapture()');
836
- manager.retryCapture();
837
- }}
838
- >
839
- 🔄 Retry Capture
840
- </button>
841
- ) : (
842
- <>
843
- <p class="no-attempts">No attempts remaining</p>
844
- <button
845
- type="button"
846
- class="btn btn-secondary"
847
- style={{ marginTop: '1rem' }}
848
- onClick={() => {
849
- log('flow', 'Continuing after max attempts reached');
850
- onContinue();
851
- }}
852
- >
853
- Continue to Next Step →
854
- </button>
855
- </>
856
- )}
857
- </div>
858
- )}
859
- </div>
860
- );
861
- }
862
-
863
- // ============================================================================
864
- // Shared UI Components
865
- // ============================================================================
866
-
867
- function LoadingUI({ message }: { message?: string }) {
868
- return (
869
- <div class="loading-container">
870
- <div class="spinner" />
871
- <p class="loading-text">{message ?? 'Loading...'}</p>
872
- </div>
873
- );
874
- }
875
-
876
- function SuccessUI({ message }: { message: string }) {
877
- return (
878
- <div class="result-container">
879
- <div class="result-icon">✅</div>
880
- <h2 class="result-title success">{message}</h2>
881
- <p class="result-description">Moving to next step...</p>
882
- </div>
883
- );
884
- }
885
-
886
- function FlowCompleteUI({
887
- finishStatus,
888
- onReset,
889
- }: {
890
- finishStatus: OrchestratedFlowState & { status: 'finished' };
891
- onReset: () => void;
892
- }) {
893
- return (
894
- <div class="result-container">
895
- <div class="result-icon">🎉</div>
896
- <h2 class="result-title success">Flow Complete!</h2>
897
- <p class="result-description">All modules completed successfully.</p>
898
-
899
- <div class="finish-data">
900
- <pre>{JSON.stringify(finishStatus.finishStatus, null, 2)}</pre>
901
- </div>
902
-
903
- <button type="button" class="btn btn-primary" onClick={onReset}>
904
- Start Over
905
- </button>
906
- </div>
907
- );
908
- }
909
-
910
- function ErrorUI({ error, onRetry }: { error: string; onRetry: () => void }) {
911
- return (
912
- <div class="result-container">
913
- <div class="result-icon">❌</div>
914
- <h2 class="result-title error">Error</h2>
915
- <p class="result-description">{error}</p>
916
- <button type="button" class="btn btn-primary" onClick={onRetry}>
917
- Try Again
918
- </button>
919
- </div>
920
- );
921
- }
922
-
923
- // ============================================================================
924
- // Module Renderer - Uses its own managers, not the orchestrated flow's internal state
925
- // ============================================================================
926
-
927
- function ModuleUI({
928
- currentStep,
929
- config,
930
- flowManager,
931
- log,
932
- }: {
933
- currentStep: string;
934
- config: FlowModuleConfig[keyof FlowModuleConfig];
935
- flowManager: FlowManager;
936
- log: (type: LogEntry['type'], msg: string) => void;
937
- }) {
938
- // Local state from our managers
939
- const [phoneState, setPhoneState] = useState<PhoneState | null>(null);
940
- const [emailState, setEmailState] = useState<EmailState | null>(null);
941
- const [selfieState, setSelfieState] = useState<SelfieState | null>(null);
942
-
943
- // Create module managers on demand
944
- const [phoneManager] = useState(() =>
945
- currentStep === 'PHONE'
946
- ? createPhoneManager({ config: config as FlowModuleConfig['PHONE'] })
947
- : null,
948
- );
949
- const [emailManager] = useState(() =>
950
- currentStep === 'EMAIL'
951
- ? createEmailManager({
952
- config: {
953
- ...(config as FlowModuleConfig['EMAIL']),
954
- prefill: (config as EmailConfig).prefill ?? false,
955
- },
956
- })
957
- : null,
958
- );
959
- const [selfieManager] = useState(() =>
960
- currentStep === 'SELFIE'
961
- ? createSelfieManager({ config: config as FlowModuleConfig['SELFIE'] })
962
- : null,
963
- );
964
-
965
- // Subscribe to module state and auto-complete
966
- useEffect(() => {
967
- if (phoneManager) {
968
- setPhoneState(phoneManager.getState());
969
- const unsub = phoneManager.subscribe((state) => {
970
- setPhoneState(state);
971
- log('state', `[PHONE] ${state.status}`);
972
- if (state.status === 'success') {
973
- setTimeout(() => {
974
- log('flow', 'Module PHONE complete');
975
- flowManager.completeModule();
976
- }, 1000);
977
- }
978
- });
979
- phoneManager.load();
980
- log('action', '[PHONE] load()');
981
- return unsub;
982
- }
983
-
984
- if (emailManager) {
985
- setEmailState(emailManager.getState());
986
- const unsub = emailManager.subscribe((state) => {
987
- setEmailState(state);
988
- log('state', `[EMAIL] ${state.status}`);
989
- if (state.status === 'success') {
990
- setTimeout(() => {
991
- log('flow', 'Module EMAIL complete');
992
- flowManager.completeModule();
993
- }, 1000);
994
- }
995
- });
996
- emailManager.load();
997
- log('action', '[EMAIL] load()');
998
- return unsub;
999
- }
1000
-
1001
- if (selfieManager) {
1002
- setSelfieState(selfieManager.getState());
1003
- const unsub = selfieManager.subscribe((state) => {
1004
- setSelfieState(state);
1005
- const extra =
1006
- state.status === 'capture' ? ` (${state.detectionStatus})` : '';
1007
- log('state', `[SELFIE] ${state.status}${extra}`);
1008
- if (state.status === 'finished') {
1009
- setTimeout(() => {
1010
- log('flow', 'Module SELFIE complete');
1011
- flowManager.completeModule();
1012
- }, 1000);
1013
- }
1014
- });
1015
- selfieManager.load();
1016
- log('action', '[SELFIE] load()');
1017
- return unsub;
1018
- }
1019
- }, [phoneManager, emailManager, selfieManager, flowManager, log]);
1020
-
1021
- // Render Phone module
1022
- if (currentStep === 'PHONE' && phoneManager && phoneState) {
1023
- switch (phoneState.status) {
1024
- case 'idle':
1025
- case 'loadingPrefill':
1026
- return <LoadingUI message="Loading phone module..." />;
1027
- case 'inputting':
1028
- return (
1029
- <PhoneInputUI state={phoneState} manager={phoneManager} log={log} />
1030
- );
1031
- case 'submitting':
1032
- case 'sendingOtp':
1033
- return <LoadingUI message="Sending code..." />;
1034
- case 'awaitingOtp':
1035
- return (
1036
- <PhoneOtpUI state={phoneState} manager={phoneManager} log={log} />
1037
- );
1038
- case 'verifyingOtp':
1039
- return <LoadingUI message="Verifying..." />;
1040
- case 'success':
1041
- return <SuccessUI message="Phone Verified!" />;
1042
- case 'error':
1043
- case 'otpError':
1044
- return (
1045
- <ErrorUI
1046
- error={phoneState.error ?? 'Unknown error'}
1047
- onRetry={() => phoneManager.reset()}
1048
- />
1049
- );
1050
- }
1051
- }
1052
-
1053
- // Render Email module
1054
- if (currentStep === 'EMAIL' && emailManager && emailState) {
1055
- switch (emailState.status) {
1056
- case 'idle':
1057
- case 'loadingPrefill':
1058
- return <LoadingUI message="Loading email module..." />;
1059
- case 'inputting':
1060
- return (
1061
- <EmailInputUI state={emailState} manager={emailManager} log={log} />
1062
- );
1063
- case 'submitting':
1064
- case 'sendingOtp':
1065
- return <LoadingUI message="Sending code..." />;
1066
- case 'awaitingOtp':
1067
- return (
1068
- <EmailOtpUI state={emailState} manager={emailManager} log={log} />
1069
- );
1070
- case 'verifyingOtp':
1071
- return <LoadingUI message="Verifying..." />;
1072
- case 'success':
1073
- return <SuccessUI message="Email Verified!" />;
1074
- case 'error':
1075
- case 'otpError':
1076
- return (
1077
- <ErrorUI
1078
- error={emailState.error ?? 'Unknown error'}
1079
- onRetry={() => emailManager.reset()}
1080
- />
1081
- );
1082
- }
1083
- }
1084
-
1085
- // Render Selfie module
1086
- if (currentStep === 'SELFIE' && selfieManager && selfieState) {
1087
- switch (selfieState.status) {
1088
- case 'idle':
1089
- case 'loading':
1090
- return <LoadingUI message="Loading WASM models..." />;
1091
- case 'tutorial':
1092
- return <SelfieTutorialUI manager={selfieManager} log={log} />;
1093
- case 'permissions':
1094
- return (
1095
- <SelfiePermissionsUI
1096
- state={selfieState}
1097
- manager={selfieManager}
1098
- log={log}
1099
- />
1100
- );
1101
- case 'capture':
1102
- return (
1103
- <SelfieCaptureUI
1104
- state={selfieState}
1105
- manager={selfieManager}
1106
- log={log}
1107
- onContinue={() => {
1108
- log('flow', 'Module SELFIE skipped (max attempts)');
1109
- flowManager.completeModule();
1110
- }}
1111
- />
1112
- );
1113
- case 'finished':
1114
- return <SuccessUI message="Selfie Captured!" />;
1115
- case 'error':
1116
- return (
1117
- <ErrorUI
1118
- error={selfieState.error ?? 'Unknown error'}
1119
- onRetry={() => selfieManager.reset()}
1120
- />
1121
- );
1122
- }
1123
- }
1124
-
1125
- return <LoadingUI message={`Loading ${currentStep}...`} />;
1126
- }
1127
-
1128
- // ============================================================================
1129
- // Main App
1130
- // ============================================================================
1131
-
1132
- function App() {
1133
- const [sessionStarted, setSessionStarted] = useState(false);
1134
- const [isStartingSession, setIsStartingSession] = useState(false);
1135
- const [configId, setConfigId] = useState(() => getConfigIdFromUrl());
1136
- const [flowState, setFlowState] = useState<OrchestratedFlowState | null>(
1137
- null,
1138
- );
1139
- const [flowManager, setFlowManager] = useState<FlowManager | null>(null);
1140
- const [logs, setLogs] = useState<LogEntry[]>([]);
1141
- const [initError, setInitError] = useState<string | null>(null);
1142
-
1143
- const log = useCallback((type: LogEntry['type'], message: string) => {
1144
- const time = new Date().toLocaleTimeString('en-US', { hour12: false });
1145
- setLogs((prev) => [{ time, type, message }, ...prev]);
1146
- }, []);
1147
-
1148
- const startSession = async () => {
1149
- setIsStartingSession(true);
1150
- try {
1151
- log('init', 'Creating session...');
1152
-
1153
- // Setup SDK (no token yet)
1154
- await setup({
1155
- apiURL: API_URL,
1156
- token: '',
1157
- wasm: WASM_CONFIG,
1158
- });
1159
-
1160
- // Create session with the configId from state
1161
- log('init', `Config ID: ${configId}`);
1162
-
1163
- const session = await createSession(API_KEY, {
1164
- configurationId: configId,
1165
- language: 'en-US',
1166
- });
1167
- log('init', `Session created: ${session.interviewId}`);
1168
-
1169
- // Re-setup with token
1170
- await setup({
1171
- apiURL: API_URL,
1172
- token: session.token,
1173
- wasm: WASM_CONFIG,
1174
- });
1175
- log('init', 'SDK configured with session token');
1176
-
1177
- // Create orchestrated flow manager
1178
- log('init', 'Creating flow manager...');
1179
- const manager = createOrchestratedFlowManager({
1180
- modules: {
1181
- PHONE: phoneMachine,
1182
- EMAIL: emailMachine,
1183
- SELFIE: selfieMachine,
1184
- },
1185
- });
1186
-
1187
- manager.subscribe((state) => {
1188
- log('flow', `Flow status: ${state.status}`);
1189
- setFlowState(state);
1190
- });
1191
-
1192
- setFlowManager(manager);
1193
- setFlowState(manager.getState());
1194
- setSessionStarted(true);
1195
-
1196
- // Load the flow
1197
- log('action', 'flowManager.load()');
1198
- manager.load();
1199
- } catch (err) {
1200
- const message = err instanceof Error ? err.message : String(err);
1201
- log('error', message);
1202
- setInitError(message);
1203
- } finally {
1204
- setIsStartingSession(false);
1205
- }
1206
- };
1207
-
1208
- const resetFlow = () => {
1209
- setSessionStarted(false);
1210
- setFlowState(null);
1211
- setFlowManager(null);
1212
- setLogs([]);
1213
- setInitError(null);
1214
- };
1215
-
1216
- // Render main content
1217
- const renderContent = () => {
1218
- // Welcome screen
1219
- if (!sessionStarted) {
1220
- if (initError) {
1221
- return <ErrorUI error={initError} onRetry={resetFlow} />;
1222
- }
1223
- return (
1224
- <WelcomeUI
1225
- onStartSession={startSession}
1226
- isStarting={isStartingSession}
1227
- configId={configId}
1228
- onConfigIdChange={setConfigId}
1229
- />
1230
- );
1231
- }
1232
-
1233
- // Flow states
1234
- if (!flowState || !flowManager) {
1235
- return <LoadingUI message="Initializing..." />;
1236
- }
1237
-
1238
- switch (flowState.status) {
1239
- case 'idle':
1240
- case 'loading':
1241
- return <LoadingUI message="Loading flow configuration..." />;
1242
-
1243
- case 'ready':
1244
- if (!flowState.currentStep) {
1245
- return <LoadingUI message="Preparing module..." />;
1246
- }
1247
- return (
1248
- <ModuleUI
1249
- key={flowState.currentStep}
1250
- currentStep={flowState.currentStep}
1251
- config={
1252
- flowState.config as FlowModuleConfig[keyof FlowModuleConfig]
1253
- }
1254
- flowManager={flowManager}
1255
- log={log}
1256
- />
1257
- );
1258
-
1259
- case 'finished':
1260
- return <FlowCompleteUI finishStatus={flowState} onReset={resetFlow} />;
1261
-
1262
- case 'error':
1263
- return <ErrorUI error={flowState.error} onRetry={resetFlow} />;
1264
- }
1265
- };
1266
-
1267
- // Get display state for right panel
1268
- const getDisplayState = () => {
1269
- if (!flowState) return { status: 'waiting' };
1270
-
1271
- if (flowState.status === 'ready') {
1272
- const moduleState = flowState.moduleState as
1273
- | { status?: string }
1274
- | undefined;
1275
- return {
1276
- flowStatus: flowState.status,
1277
- currentStep: flowState.currentStep,
1278
- stepIndex: `${flowState.currentStepIndex + 1}/${flowState.steps.length}`,
1279
- moduleStatus: moduleState?.status,
1280
- };
1281
- }
1282
-
1283
- return flowState;
1284
- };
1285
-
1286
- return (
1287
- <div class="layout">
1288
- {/* LEFT: Custom UI */}
1289
- <div class="ui-panel">
1290
- <div class="ui-panel-header">
1291
- <div class="ui-badge">
1292
- <span>🎨</span> Your Custom UI
1293
- </div>
1294
- <h1 class="ui-title">Headless SDK</h1>
1295
- <p class="ui-subtitle">
1296
- 100% custom design • SDK handles logic • You handle pixels
1297
- </p>
1298
- </div>
1299
-
1300
- {/* Flow progress indicator */}
1301
- {flowState?.status === 'ready' && (
1302
- <FlowProgress
1303
- steps={flowState.steps}
1304
- currentIndex={flowState.currentStepIndex}
1305
- />
1306
- )}
1307
-
1308
- <div class="ui-content">{renderContent()}</div>
1309
- </div>
1310
-
1311
- {/* RIGHT: SDK State */}
1312
- <div class="sdk-panel">
1313
- <div class="sdk-panel-header">
1314
- <div class="sdk-badge">
1315
- <span>⚡</span> SDK State
1316
- </div>
1317
- <h2 class="sdk-title">Under the Hood</h2>
1318
- <p class="sdk-subtitle">@incodetech/core/flow</p>
1319
- </div>
1320
-
1321
- <StateViewer state={getDisplayState()} label="Current State" />
1322
- <EventLog entries={logs} />
1323
- </div>
1324
- </div>
1325
- );
1326
- }
1327
-
1328
- // Mount
1329
- const root = document.getElementById('root');
1330
- if (root) {
1331
- render(<App />, root);
1332
- }