@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.
- package/dist/asset-manifest.json +16 -16
- package/package.json +5 -2
- package/.turbo/turbo-build.log +0 -58
- package/.turbo/turbo-coverage.log +0 -23
- package/.turbo/turbo-format.log +0 -6
- package/.turbo/turbo-lint$colon$fix.log +0 -6
- package/.turbo/turbo-lint.log +0 -6
- package/.turbo/turbo-test.log +0 -1033
- package/.turbo/turbo-typecheck.log +0 -5
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/email/email.tsx.html +0 -850
- package/coverage/email/emailInput.tsx.html +0 -340
- package/coverage/email/index.html +0 -131
- package/coverage/favicon.png +0 -0
- package/coverage/flow/flow.tsx.html +0 -961
- package/coverage/flow/flowCompleted.tsx.html +0 -448
- package/coverage/flow/flowInit.ts.html +0 -367
- package/coverage/flow/flowStart.tsx.html +0 -208
- package/coverage/flow/index.html +0 -221
- package/coverage/flow/preloadFlow.ts.html +0 -598
- package/coverage/flow/unsupportedModule.tsx.html +0 -202
- package/coverage/flow/useFlowInitialization.ts.html +0 -469
- package/coverage/flow/useModuleLoader.ts.html +0 -361
- package/coverage/hooks/index.html +0 -116
- package/coverage/hooks/useManager.ts.html +0 -205
- package/coverage/index.html +0 -401
- package/coverage/permissions/boldWithArrow.tsx.html +0 -208
- package/coverage/permissions/denied.tsx.html +0 -172
- package/coverage/permissions/deniedAndroid.tsx.html +0 -253
- package/coverage/permissions/deniedDesktop.tsx.html +0 -277
- package/coverage/permissions/deniedIOS.tsx.html +0 -304
- package/coverage/permissions/deniedInstructions.tsx.html +0 -142
- package/coverage/permissions/iconWrapper.tsx.html +0 -130
- package/coverage/permissions/index.html +0 -251
- package/coverage/permissions/learnMore.tsx.html +0 -340
- package/coverage/permissions/numberedStep.tsx.html +0 -127
- package/coverage/permissions/permissions.tsx.html +0 -289
- package/coverage/phone/index.html +0 -116
- package/coverage/phone/phoneInput.tsx.html +0 -832
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/selfie/index.html +0 -131
- package/coverage/selfie/selfie.tsx.html +0 -334
- package/coverage/selfie/tutorial.tsx.html +0 -214
- package/coverage/shared/baseTutorial/baseTutorial.tsx.html +0 -250
- package/coverage/shared/baseTutorial/index.html +0 -131
- package/coverage/shared/baseTutorial/replaceBaseTutorial.ts.html +0 -289
- package/coverage/shared/button/button.tsx.html +0 -226
- package/coverage/shared/button/index.html +0 -116
- package/coverage/shared/componentRoot/incodeComponent.tsx.html +0 -121
- package/coverage/shared/componentRoot/index.html +0 -116
- package/coverage/shared/countries/countries.ts.html +0 -502
- package/coverage/shared/countries/index.html +0 -116
- package/coverage/shared/icons/chevronDown.tsx.html +0 -151
- package/coverage/shared/icons/index.html +0 -131
- package/coverage/shared/icons/successIcon.tsx.html +0 -163
- package/coverage/shared/loader/index.html +0 -116
- package/coverage/shared/loader/loadingIcon.tsx.html +0 -286
- package/coverage/shared/otpInput/index.html +0 -116
- package/coverage/shared/otpInput/otpInput.tsx.html +0 -808
- package/coverage/shared/page/index.html +0 -146
- package/coverage/shared/page/page.tsx.html +0 -358
- package/coverage/shared/page/pageUiConfig.ts.html +0 -277
- package/coverage/shared/page/verifiedByIncode.tsx.html +0 -310
- package/coverage/shared/spacer/index.html +0 -116
- package/coverage/shared/spacer/spacer.tsx.html +0 -349
- package/coverage/shared/spinner/index.html +0 -116
- package/coverage/shared/spinner/spinner.tsx.html +0 -280
- package/coverage/shared/title/index.html +0 -116
- package/coverage/shared/title/title.tsx.html +0 -121
- package/coverage/shared/uiConfig/index.html +0 -116
- package/coverage/shared/uiConfig/uiConfig.ts.html +0 -193
- package/coverage/shared/webComponent/incodeModule.ts.html +0 -172
- package/coverage/shared/webComponent/index.html +0 -131
- package/coverage/shared/webComponent/registerIncodeElement.ts.html +0 -130
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/styles/cn.tsx.html +0 -148
- package/coverage/styles/fetchTheme.ts.html +0 -349
- package/coverage/styles/index.html +0 -131
- package/dev/README.md +0 -163
- package/dev/getToken.ts +0 -36
- package/dev/headless.html +0 -875
- package/dev/index.html +0 -366
- package/dev/main-headless.tsx +0 -1332
- package/dev/main-orchestrated-flow.tsx +0 -1158
- package/dev/main-preact.tsx +0 -323
- package/dev/main-simplified.tsx +0 -123
- package/dev/main-web-component.tsx +0 -256
- package/dev/main.tsx +0 -332
- package/dev/manual.html +0 -27
- package/dev/orchestrated-flow.html +0 -64
- package/dev/simplified.html +0 -64
- package/dev/tiktok-logo.svg +0 -7
- package/src/defineCustomElement.tsx +0 -30
- package/src/email/email.test.tsx +0 -368
- package/src/email/email.tsx +0 -255
- package/src/email/emailInput.test.tsx +0 -264
- package/src/email/emailInput.tsx +0 -85
- package/src/email/styles.css +0 -59
- package/src/flow/flow.test.tsx +0 -796
- package/src/flow/flow.tsx +0 -292
- package/src/flow/flowCompleted.css +0 -30
- package/src/flow/flowCompleted.test.tsx +0 -331
- package/src/flow/flowCompleted.tsx +0 -121
- package/src/flow/flowInit.test.ts +0 -264
- package/src/flow/flowInit.ts +0 -94
- package/src/flow/flowStart.css +0 -58
- package/src/flow/flowStart.test.tsx +0 -49
- package/src/flow/flowStart.tsx +0 -41
- package/src/flow/incode-logo.svg +0 -8
- package/src/flow/index.ts +0 -7
- package/src/flow/preloadFlow.test.ts +0 -421
- package/src/flow/preloadFlow.ts +0 -171
- package/src/flow/styles.css +0 -9
- package/src/flow/unsupportedModule.css +0 -21
- package/src/flow/unsupportedModule.tsx +0 -39
- package/src/flow/useFlowInitialization.test.tsx +0 -292
- package/src/flow/useFlowInitialization.ts +0 -128
- package/src/flow/useModuleLoader.test.tsx +0 -212
- package/src/flow/useModuleLoader.ts +0 -92
- package/src/hooks/index.ts +0 -1
- package/src/hooks/useManager.test.ts +0 -91
- package/src/hooks/useManager.ts +0 -40
- package/src/i18n/index.ts +0 -3
- package/src/i18n/instance.ts +0 -16
- package/src/i18n/setup.ts +0 -184
- package/src/i18n/useTranslation.ts +0 -42
- package/src/index.ts +0 -27
- package/src/permissions/assets/android-dots-icon.svg +0 -7
- package/src/permissions/assets/android-settings-icon.svg +0 -16
- package/src/permissions/assets/android-toggle-icon.svg +0 -20
- package/src/permissions/assets/bank-card-icon.svg +0 -14
- package/src/permissions/assets/camera-icon.svg +0 -12
- package/src/permissions/assets/camera-ios.svg +0 -53
- package/src/permissions/assets/check-icon.svg +0 -8
- package/src/permissions/assets/chrome-icon.svg +0 -43
- package/src/permissions/assets/password-icon.svg +0 -11
- package/src/permissions/assets/permissions-img.svg +0 -51
- package/src/permissions/assets/safari-icon.svg +0 -37
- package/src/permissions/assets/settings-icon.svg +0 -33
- package/src/permissions/assets/toggle-icon.svg +0 -19
- package/src/permissions/assets/warning-icon.svg +0 -6
- package/src/permissions/boldWithArrow.css +0 -9
- package/src/permissions/boldWithArrow.tsx +0 -41
- package/src/permissions/denied.css +0 -37
- package/src/permissions/denied.tsx +0 -29
- package/src/permissions/deniedAndroid.tsx +0 -56
- package/src/permissions/deniedDesktop.css +0 -9
- package/src/permissions/deniedDesktop.tsx +0 -64
- package/src/permissions/deniedIOS.tsx +0 -73
- package/src/permissions/deniedInstructions.tsx +0 -19
- package/src/permissions/iconWrapper.css +0 -9
- package/src/permissions/iconWrapper.tsx +0 -15
- package/src/permissions/learnMore.css +0 -37
- package/src/permissions/learnMore.tsx +0 -85
- package/src/permissions/numberedStep.css +0 -13
- package/src/permissions/numberedStep.tsx +0 -14
- package/src/permissions/permissions.css +0 -13
- package/src/permissions/permissions.tsx +0 -68
- package/src/phone/phone.tsx +0 -246
- package/src/phone/phoneInput.test.tsx +0 -275
- package/src/phone/phoneInput.tsx +0 -249
- package/src/phone/styles.css +0 -158
- package/src/selfie/cameraButton.css +0 -13
- package/src/selfie/cameraButton.tsx +0 -35
- package/src/selfie/capture.css +0 -57
- package/src/selfie/capture.tsx +0 -232
- package/src/selfie/errorModal.tsx +0 -218
- package/src/selfie/errorModalContent.css +0 -33
- package/src/selfie/errorModalContent.tsx +0 -44
- package/src/selfie/faceOutline.css +0 -5
- package/src/selfie/faceOutline.tsx +0 -22
- package/src/selfie/loadingBorder.css +0 -12
- package/src/selfie/loadingBorder.tsx +0 -77
- package/src/selfie/manualCaptureButton.css +0 -13
- package/src/selfie/manualCaptureButton.tsx +0 -35
- package/src/selfie/noMoreAttemptsModal.tsx +0 -44
- package/src/selfie/notification.css +0 -9
- package/src/selfie/notification.tsx +0 -36
- package/src/selfie/retryErrorModal.tsx +0 -56
- package/src/selfie/selfie.test.tsx +0 -458
- package/src/selfie/selfie.tsx +0 -83
- package/src/selfie/selfieTutorial.json +0 -2626
- package/src/selfie/styles.css +0 -1
- package/src/selfie/tutorial.test.tsx +0 -200
- package/src/selfie/tutorial.tsx +0 -43
- package/src/setup.ts +0 -33
- package/src/shared/baseTutorial/baseTutorial.css +0 -21
- package/src/shared/baseTutorial/baseTutorial.test.tsx +0 -184
- package/src/shared/baseTutorial/baseTutorial.tsx +0 -55
- package/src/shared/baseTutorial/replaceBaseTutorial.test.ts +0 -267
- package/src/shared/baseTutorial/replaceBaseTutorial.ts +0 -68
- package/src/shared/button/button.css +0 -55
- package/src/shared/button/button.test.tsx +0 -101
- package/src/shared/button/button.tsx +0 -47
- package/src/shared/componentRoot/incodeComponent.tsx +0 -12
- package/src/shared/countries/countries.test.ts +0 -75
- package/src/shared/countries/countries.ts +0 -139
- package/src/shared/countries/index.ts +0 -6
- package/src/shared/icons/chevronDown.tsx +0 -22
- package/src/shared/icons/index.ts +0 -2
- package/src/shared/icons/successIcon.css +0 -5
- package/src/shared/icons/successIcon.test.tsx +0 -40
- package/src/shared/icons/successIcon.tsx +0 -26
- package/src/shared/loader/loadingIcon.css +0 -28
- package/src/shared/loader/loadingIcon.tsx +0 -67
- package/src/shared/lottie/lottie.tsx +0 -108
- package/src/shared/otpInput/otpInput.css +0 -85
- package/src/shared/otpInput/otpInput.test.tsx +0 -356
- package/src/shared/otpInput/otpInput.tsx +0 -241
- package/src/shared/page/incode-logo.svg +0 -3
- package/src/shared/page/page.css +0 -47
- package/src/shared/page/page.test.tsx +0 -97
- package/src/shared/page/page.tsx +0 -91
- package/src/shared/page/pageUiConfig.test.ts +0 -112
- package/src/shared/page/pageUiConfig.ts +0 -64
- package/src/shared/page/verifiedByIncode.css +0 -5
- package/src/shared/page/verifiedByIncode.tsx +0 -75
- package/src/shared/spacer/spacer.css +0 -149
- package/src/shared/spacer/spacer.test.tsx +0 -143
- package/src/shared/spacer/spacer.tsx +0 -88
- package/src/shared/spinner/index.ts +0 -2
- package/src/shared/spinner/spinner.css +0 -28
- package/src/shared/spinner/spinner.test.tsx +0 -82
- package/src/shared/spinner/spinner.tsx +0 -65
- package/src/shared/title/title.css +0 -7
- package/src/shared/title/title.tsx +0 -12
- package/src/shared/uiConfig/uiConfig.ts +0 -36
- package/src/shared/webComponent/incodeModule.ts +0 -29
- package/src/shared/webComponent/registerIncodeElement.ts +0 -15
- package/src/styles/__mocks__/fetchTheme.ts +0 -19
- package/src/styles/applyTheme.ts +0 -37
- package/src/styles/cn.test.tsx +0 -57
- package/src/styles/cn.tsx +0 -21
- package/src/styles/core.css +0 -12
- package/src/styles/fetchTheme.test.ts +0 -390
- package/src/styles/fetchTheme.ts +0 -88
- package/src/styles/generatePalette.ts +0 -111
- package/src/styles/reset.css +0 -65
- package/src/styles/resolveCssVariableToHex.ts +0 -28
- package/src/styles/tailwind.css +0 -291
- package/src/styles/themeTypes.ts +0 -18
- package/src/styles/tokens/colors.css +0 -190
- package/src/styles/tokens/components.css +0 -174
- package/src/styles/tokens/index.css +0 -4
- package/src/styles/tokens/primitives.css +0 -129
- package/src/styles/tokens/semantic.css +0 -51
- package/src/svg.d.ts +0 -4
- package/src/types/assets.d.ts +0 -1
- package/src/types/custom-elements.d.ts +0 -104
- package/tsconfig.json +0 -22
- package/vite.config.ts +0 -260
- package/vitest.config.ts +0 -40
- package/vitest.setup.ts +0 -16
package/dev/main-headless.tsx
DELETED
|
@@ -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
|
-
}
|