@casperid/react 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_commonjsHelpers-DKOUU3wS.cjs +2 -0
- package/dist/_commonjsHelpers-DKOUU3wS.cjs.map +1 -0
- package/dist/_commonjsHelpers-DaMA6jEr.js +9 -0
- package/dist/_commonjsHelpers-DaMA6jEr.js.map +1 -0
- package/dist/camera_utils-BQaOSBPu.js +397 -0
- package/dist/camera_utils-BQaOSBPu.js.map +1 -0
- package/dist/camera_utils-wchtqrQn.cjs +2 -0
- package/dist/camera_utils-wchtqrQn.cjs.map +1 -0
- package/dist/face_mesh-DYMPc5Ce.js +4212 -0
- package/dist/face_mesh-DYMPc5Ce.js.map +1 -0
- package/dist/face_mesh-fEvyDoPt.cjs +19 -0
- package/dist/face_mesh-fEvyDoPt.cjs.map +1 -0
- package/dist/index.cjs +41 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1786 -1501
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +11 -11
- package/src/CasperIDModal.tsx +0 -777
- package/src/index.css +0 -217
- package/src/index.ts +0 -41
- package/src/screens/AuthSelection.tsx +0 -221
- package/src/screens/DocumentScan.tsx +0 -348
- package/src/screens/FaceScan.tsx +0 -368
- package/src/screens/IdentityVerified.tsx +0 -51
- package/src/screens/L1Setup.tsx +0 -335
- package/src/screens/Login.tsx +0 -186
- package/src/screens/MintingIdentity.tsx +0 -446
- package/src/screens/PasskeyAuth.tsx +0 -259
- package/src/screens/PasskeyRegister.tsx +0 -281
- package/src/screens/PermissionsRequest.tsx +0 -96
- package/src/screens/PinVerification.tsx +0 -321
- package/src/screens/ReviewData.tsx +0 -315
- package/src/screens/SecurityUpgrade.tsx +0 -83
- package/src/screens/VerifyIdentityChoice.tsx +0 -59
- package/src/shared/Footer.tsx +0 -13
- package/src/shared/GlassContainer.tsx +0 -17
- package/src/shared/Header.tsx +0 -62
- package/src/types.ts +0 -342
- package/src/utils/fetchWithTimeout.ts +0 -52
- package/src/utils/i18n.ts +0 -640
- package/src/utils/webauthn.ts +0 -261
package/src/CasperIDModal.tsx
DELETED
|
@@ -1,777 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import { AnimatePresence, motion } from 'framer-motion';
|
|
3
|
-
|
|
4
|
-
import GlassContainer from './shared/GlassContainer';
|
|
5
|
-
import Header from './shared/Header';
|
|
6
|
-
import Footer from './shared/Footer';
|
|
7
|
-
|
|
8
|
-
import AuthSelection from './screens/AuthSelection';
|
|
9
|
-
import Login from './screens/Login';
|
|
10
|
-
import PasskeyAuth from './screens/PasskeyAuth';
|
|
11
|
-
import PasskeyRegister from './screens/PasskeyRegister';
|
|
12
|
-
import PinVerification from './screens/PinVerification';
|
|
13
|
-
import SecurityUpgrade from './screens/SecurityUpgrade';
|
|
14
|
-
import FaceScan from './screens/FaceScan';
|
|
15
|
-
import VerifyIdentityChoice from './screens/VerifyIdentityChoice';
|
|
16
|
-
import DocumentScan from './screens/DocumentScan';
|
|
17
|
-
import ReviewData from './screens/ReviewData';
|
|
18
|
-
import MintingIdentity from './screens/MintingIdentity';
|
|
19
|
-
import IdentityVerified from './screens/IdentityVerified';
|
|
20
|
-
import PermissionsRequest from './screens/PermissionsRequest';
|
|
21
|
-
|
|
22
|
-
import type {
|
|
23
|
-
CasperIDModalProps,
|
|
24
|
-
Screen,
|
|
25
|
-
SDKMode,
|
|
26
|
-
OTPState,
|
|
27
|
-
KYCState,
|
|
28
|
-
DocumentType,
|
|
29
|
-
ExtractedDocumentData,
|
|
30
|
-
StartVerificationResponse,
|
|
31
|
-
AuthState,
|
|
32
|
-
PasskeyAuthCompleteResponse,
|
|
33
|
-
PasskeyRegisterCompleteResponse,
|
|
34
|
-
AppMetadata,
|
|
35
|
-
AppMetadataResponse,
|
|
36
|
-
CasperIDTheme
|
|
37
|
-
} from './types';
|
|
38
|
-
|
|
39
|
-
// Resolve 'system' mode to actual light/dark
|
|
40
|
-
function resolveMode(mode: SDKMode = 'light'): 'light' | 'dark' {
|
|
41
|
-
if (mode === 'system') {
|
|
42
|
-
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
43
|
-
}
|
|
44
|
-
return mode;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const FOOTER_SCREENS: Screen[] = ['AuthSelection', 'Login', 'PasskeyAuth', 'PinVerification', 'PasskeyRegister'];
|
|
48
|
-
|
|
49
|
-
// Default API base URL
|
|
50
|
-
const DEFAULT_API_BASE_URL = 'https://apis.casperid.com';
|
|
51
|
-
|
|
52
|
-
// Initial KYC state
|
|
53
|
-
const initialKYCState: KYCState = {
|
|
54
|
-
requestId: '',
|
|
55
|
-
verificationTier: 'layer_3'
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
export const CasperIDModal: React.FC<CasperIDModalProps> = ({
|
|
59
|
-
isOpen,
|
|
60
|
-
apiKey,
|
|
61
|
-
theme = {},
|
|
62
|
-
requiredTier,
|
|
63
|
-
onSuccess,
|
|
64
|
-
onClose,
|
|
65
|
-
onError,
|
|
66
|
-
mode = 'login',
|
|
67
|
-
previewMode = false,
|
|
68
|
-
initialScreen,
|
|
69
|
-
apiBaseUrl,
|
|
70
|
-
termsUrl = '#',
|
|
71
|
-
privacyUrl = '#',
|
|
72
|
-
logoUrl,
|
|
73
|
-
language = 'EN',
|
|
74
|
-
layout,
|
|
75
|
-
}) => {
|
|
76
|
-
const [cloudTheme, setCloudTheme] = useState<CasperIDTheme>({});
|
|
77
|
-
// Track loading state for cloud theme - prevents flash of default colors
|
|
78
|
-
const [isLoadingCloudTheme, setIsLoadingCloudTheme] = useState(!previewMode && !!apiKey);
|
|
79
|
-
|
|
80
|
-
const {
|
|
81
|
-
primaryColor: propPrimaryColor,
|
|
82
|
-
borderRadius: propBorderRadius,
|
|
83
|
-
fontFamily: propFontFamily,
|
|
84
|
-
mode: themeMode = 'light',
|
|
85
|
-
layout: propLayout,
|
|
86
|
-
} = theme;
|
|
87
|
-
|
|
88
|
-
// Merge cloud theme with props (props take precedence)
|
|
89
|
-
const primaryColor = propPrimaryColor || cloudTheme.primaryColor || '#8651e1';
|
|
90
|
-
const borderRadius = propBorderRadius || cloudTheme.borderRadius || '12';
|
|
91
|
-
const fontFamily = propFontFamily || cloudTheme.fontFamily || 'Inter';
|
|
92
|
-
const finalLayout = layout || propLayout || cloudTheme.layout || 'auto';
|
|
93
|
-
|
|
94
|
-
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(resolveMode(themeMode));
|
|
95
|
-
const [screen, setScreen] = useState<Screen>(
|
|
96
|
-
initialScreen ?? (mode === 'verify' ? 'SecurityUpgrade' : 'AuthSelection')
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
// OTP flow state
|
|
100
|
-
const [otpState, setOtpState] = useState<OTPState>({ email: '', verificationId: '' });
|
|
101
|
-
const [sessionToken, setSessionToken] = useState<string | null>(null);
|
|
102
|
-
|
|
103
|
-
// Auth state - tracks user status through the flow
|
|
104
|
-
const [authState, setAuthState] = useState<AuthState>({
|
|
105
|
-
email: '',
|
|
106
|
-
userExists: false,
|
|
107
|
-
hasPasskey: false,
|
|
108
|
-
userAccount: null
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// KYC flow state
|
|
112
|
-
const [kycState, setKycState] = useState<KYCState>(initialKYCState);
|
|
113
|
-
const [uploadId, setUploadId] = useState<string>('');
|
|
114
|
-
const [credentialHash, setCredentialHash] = useState<string>('');
|
|
115
|
-
const [appMetadata, setAppMetadata] = useState<AppMetadata | null>(null);
|
|
116
|
-
|
|
117
|
-
// API base URL - use custom URL if provided, otherwise default
|
|
118
|
-
const finalApiBaseUrl = apiBaseUrl || DEFAULT_API_BASE_URL;
|
|
119
|
-
|
|
120
|
-
// Normalize border radius to always have 'px' suffix
|
|
121
|
-
const normalizedRadius = borderRadius.toString().endsWith('px')
|
|
122
|
-
? borderRadius.toString()
|
|
123
|
-
: `${borderRadius}px`;
|
|
124
|
-
|
|
125
|
-
// Normalize font family
|
|
126
|
-
const normalizedFont = fontFamily.includes(' ')
|
|
127
|
-
? `"${fontFamily}", sans-serif`
|
|
128
|
-
: `${fontFamily}, sans-serif`;
|
|
129
|
-
|
|
130
|
-
// Generate a unique ID for scoped styles
|
|
131
|
-
const modalId = React.useId().replace(/:/g, '');
|
|
132
|
-
|
|
133
|
-
// Apply CSS variables + theme class to the modal root
|
|
134
|
-
const cssVars = {
|
|
135
|
-
'--casperid-primary': primaryColor,
|
|
136
|
-
'--casperid-radius': normalizedRadius,
|
|
137
|
-
'--casperid-font': normalizedFont,
|
|
138
|
-
} as React.CSSProperties;
|
|
139
|
-
|
|
140
|
-
// Generate a unique key for the style element to force re-application when theme changes
|
|
141
|
-
const styleKey = `${modalId}-${primaryColor}-${normalizedRadius}-${normalizedFont}`;
|
|
142
|
-
|
|
143
|
-
// Generate scoped CSS for proper variable inheritance with !important to override :root defaults
|
|
144
|
-
const scopedStyles = `
|
|
145
|
-
#casperid-modal-${modalId},
|
|
146
|
-
#casperid-modal-${modalId} * {
|
|
147
|
-
--casperid-primary: ${primaryColor} !important;
|
|
148
|
-
--casperid-radius: ${normalizedRadius} !important;
|
|
149
|
-
--casperid-font: ${normalizedFont} !important;
|
|
150
|
-
}
|
|
151
|
-
`;
|
|
152
|
-
|
|
153
|
-
// Sync system theme
|
|
154
|
-
useEffect(() => {
|
|
155
|
-
if (themeMode === 'system') {
|
|
156
|
-
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
157
|
-
const handler = (e: MediaQueryListEvent) => setResolvedTheme(e.matches ? 'dark' : 'light');
|
|
158
|
-
mq.addEventListener('change', handler);
|
|
159
|
-
return () => mq.removeEventListener('change', handler);
|
|
160
|
-
} else {
|
|
161
|
-
setResolvedTheme(resolveMode(themeMode));
|
|
162
|
-
}
|
|
163
|
-
}, [themeMode]);
|
|
164
|
-
|
|
165
|
-
// Sync screen in previewMode when initialScreen prop changes
|
|
166
|
-
useEffect(() => {
|
|
167
|
-
if (previewMode && initialScreen) {
|
|
168
|
-
setScreen(initialScreen);
|
|
169
|
-
}
|
|
170
|
-
}, [previewMode, initialScreen]);
|
|
171
|
-
|
|
172
|
-
// Reset to first screen when opened (for real usage)
|
|
173
|
-
useEffect(() => {
|
|
174
|
-
if (isOpen && !previewMode) {
|
|
175
|
-
setScreen(initialScreen ?? (mode === 'verify' ? 'SecurityUpgrade' : 'AuthSelection'));
|
|
176
|
-
setOtpState({ email: '', verificationId: '' });
|
|
177
|
-
setSessionToken(null);
|
|
178
|
-
setAuthState({ email: '', userExists: false, hasPasskey: false, userAccount: null });
|
|
179
|
-
setKycState(initialKYCState);
|
|
180
|
-
setUploadId('');
|
|
181
|
-
setCredentialHash('');
|
|
182
|
-
}
|
|
183
|
-
}, [isOpen, mode, previewMode, initialScreen]);
|
|
184
|
-
|
|
185
|
-
// Fetch App Metadata on mount
|
|
186
|
-
useEffect(() => {
|
|
187
|
-
if (!apiKey || previewMode) {
|
|
188
|
-
setIsLoadingCloudTheme(false);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const fetchAppMetadata = async () => {
|
|
193
|
-
try {
|
|
194
|
-
const response = await fetch(`${finalApiBaseUrl}/api/business/app-metadata/${apiKey}`);
|
|
195
|
-
if (response.ok) {
|
|
196
|
-
const data: AppMetadataResponse = await response.json();
|
|
197
|
-
if (data.success && data.app) {
|
|
198
|
-
setAppMetadata(data.app);
|
|
199
|
-
// Apply brand theme from cloud
|
|
200
|
-
if (data.app.theme) {
|
|
201
|
-
setCloudTheme({
|
|
202
|
-
primaryColor: data.app.theme.primaryColor,
|
|
203
|
-
borderRadius: data.app.theme.borderRadius,
|
|
204
|
-
fontFamily: data.app.theme.fontFamily,
|
|
205
|
-
layout: data.app.theme.layout
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
} catch (err) {
|
|
211
|
-
console.warn('[CasperID] Failed to fetch app metadata:', err);
|
|
212
|
-
} finally {
|
|
213
|
-
setIsLoadingCloudTheme(false);
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
fetchAppMetadata();
|
|
218
|
-
}, [apiKey, finalApiBaseUrl, previewMode]);
|
|
219
|
-
|
|
220
|
-
const toggleTheme = () =>
|
|
221
|
-
setResolvedTheme((prev) => (prev === 'dark' ? 'light' : 'dark'));
|
|
222
|
-
|
|
223
|
-
const go = useCallback((next: Screen) => setScreen(next), []);
|
|
224
|
-
|
|
225
|
-
// Start KYC verification flow
|
|
226
|
-
const startVerification = useCallback(async (tier: 'layer_2' | 'layer_3') => {
|
|
227
|
-
try {
|
|
228
|
-
// Use the correct endpoint for each layer
|
|
229
|
-
const endpoint = tier === 'layer_2'
|
|
230
|
-
? `${apiBaseUrl}/api/v2/verification/layer2/start`
|
|
231
|
-
: `${apiBaseUrl}/api/v2/verification/layer3/start`;
|
|
232
|
-
|
|
233
|
-
const response = await fetch(endpoint, {
|
|
234
|
-
method: 'POST',
|
|
235
|
-
headers: {
|
|
236
|
-
'Content-Type': 'application/json',
|
|
237
|
-
'X-API-Key': apiKey
|
|
238
|
-
},
|
|
239
|
-
credentials: 'include',
|
|
240
|
-
body: JSON.stringify({
|
|
241
|
-
purpose: 'sdk_verification',
|
|
242
|
-
...(tier === 'layer_3' ? { verificationMethod: 'document' } : {})
|
|
243
|
-
})
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
if (!response.ok) {
|
|
247
|
-
const errorData = await response.json().catch(() => ({}));
|
|
248
|
-
throw new Error(errorData.error || `Request failed with status ${response.status}`);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const data: StartVerificationResponse = await response.json();
|
|
252
|
-
|
|
253
|
-
if (data.success && data.verification) {
|
|
254
|
-
setKycState(prev => ({
|
|
255
|
-
...prev,
|
|
256
|
-
requestId: data.verification!.requestId,
|
|
257
|
-
verificationTier: tier
|
|
258
|
-
}));
|
|
259
|
-
return data.verification.requestId;
|
|
260
|
-
} else {
|
|
261
|
-
throw new Error(data.error || 'Failed to start verification');
|
|
262
|
-
}
|
|
263
|
-
} catch (err) {
|
|
264
|
-
onError?.(err instanceof Error ? err : new Error('Failed to start verification'));
|
|
265
|
-
return null;
|
|
266
|
-
}
|
|
267
|
-
}, [apiBaseUrl, apiKey, onError]);
|
|
268
|
-
|
|
269
|
-
// Handle successful OTP verification - new user needs passkey registration
|
|
270
|
-
const handleOTPSuccess = useCallback((token: string, userAccount?: AuthState['userAccount'], businessToken?: string) => {
|
|
271
|
-
setSessionToken(token);
|
|
272
|
-
setAuthState(prev => ({
|
|
273
|
-
...prev,
|
|
274
|
-
sessionToken: token,
|
|
275
|
-
businessToken: businessToken,
|
|
276
|
-
userAccount: userAccount || null
|
|
277
|
-
}));
|
|
278
|
-
|
|
279
|
-
// Check if user already has L1 setup (wallet + DID)
|
|
280
|
-
if (userAccount?.wallet && userAccount?.did_address) {
|
|
281
|
-
// User has L1, check if they need passkey registration
|
|
282
|
-
if (!authState.hasPasskey) {
|
|
283
|
-
go('PasskeyRegister');
|
|
284
|
-
} else {
|
|
285
|
-
// Has passkey and L1, check tier requirements
|
|
286
|
-
proceedBasedOnTier(userAccount.tier);
|
|
287
|
-
}
|
|
288
|
-
} else {
|
|
289
|
-
// New user - needs passkey registration then L1 setup
|
|
290
|
-
go('PasskeyRegister');
|
|
291
|
-
}
|
|
292
|
-
}, [go, authState.hasPasskey]);
|
|
293
|
-
|
|
294
|
-
// Handle passkey user detected (returning user with passkey)
|
|
295
|
-
const handlePasskeyUser = useCallback((email: string) => {
|
|
296
|
-
setAuthState(prev => ({ ...prev, email, hasPasskey: true, userExists: true }));
|
|
297
|
-
go('PasskeyAuth');
|
|
298
|
-
}, [go]);
|
|
299
|
-
|
|
300
|
-
// Handle successful passkey authentication (returning user)
|
|
301
|
-
const handlePasskeyAuthSuccess = useCallback((token: string, user?: PasskeyAuthCompleteResponse['user'], businessToken?: string) => {
|
|
302
|
-
setSessionToken(token);
|
|
303
|
-
setAuthState(prev => ({
|
|
304
|
-
...prev,
|
|
305
|
-
sessionToken: token,
|
|
306
|
-
businessToken: businessToken,
|
|
307
|
-
userAccount: user ? {
|
|
308
|
-
user_id: user.user_id,
|
|
309
|
-
wallet: user.wallet,
|
|
310
|
-
tier: user.tier,
|
|
311
|
-
did_address: user.did_address
|
|
312
|
-
} : null
|
|
313
|
-
}));
|
|
314
|
-
|
|
315
|
-
// Returning user - check tier requirements
|
|
316
|
-
proceedBasedOnTier(user?.tier);
|
|
317
|
-
}, []);
|
|
318
|
-
|
|
319
|
-
// Handle successful passkey registration (new user)
|
|
320
|
-
const handlePasskeyRegisterSuccess = useCallback((user?: PasskeyRegisterCompleteResponse['user'], businessToken?: string) => {
|
|
321
|
-
setAuthState(prev => ({
|
|
322
|
-
...prev,
|
|
323
|
-
hasPasskey: true,
|
|
324
|
-
businessToken: businessToken,
|
|
325
|
-
userAccount: user ? {
|
|
326
|
-
user_id: user.user_id,
|
|
327
|
-
tier: user.tier
|
|
328
|
-
} : prev.userAccount
|
|
329
|
-
}));
|
|
330
|
-
|
|
331
|
-
// After passkey registration, proceed to MintingIdentity for L1 setup
|
|
332
|
-
go('MintingIdentity');
|
|
333
|
-
}, [go]);
|
|
334
|
-
|
|
335
|
-
// Determine next step based on user's tier and required tier
|
|
336
|
-
const proceedBasedOnTier = useCallback((currentTier?: string) => {
|
|
337
|
-
const tier = currentTier || 'layer_0';
|
|
338
|
-
|
|
339
|
-
// If no specific tier required, or user meets requirement, go to success
|
|
340
|
-
if (!requiredTier) {
|
|
341
|
-
go('IdentityVerified');
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Map requiredTier to layer format
|
|
346
|
-
const requiredLayer = requiredTier === 'L1' ? 'layer_1' :
|
|
347
|
-
requiredTier === 'L2' ? 'layer_2' :
|
|
348
|
-
requiredTier === 'L3' ? 'layer_3' : 'layer_1';
|
|
349
|
-
|
|
350
|
-
// Check if user already meets the requirement
|
|
351
|
-
const tierOrder = ['layer_0', 'layer_1', 'layer_2', 'layer_3'];
|
|
352
|
-
const currentIndex = tierOrder.indexOf(tier);
|
|
353
|
-
const requiredIndex = tierOrder.indexOf(requiredLayer);
|
|
354
|
-
|
|
355
|
-
if (currentIndex >= requiredIndex) {
|
|
356
|
-
// User meets requirement
|
|
357
|
-
go('IdentityVerified');
|
|
358
|
-
} else if (requiredIndex >= 2) {
|
|
359
|
-
// Need L2 or L3 - start with SecurityUpgrade prompt
|
|
360
|
-
go('SecurityUpgrade');
|
|
361
|
-
} else {
|
|
362
|
-
// Just need L1, which should be done by now
|
|
363
|
-
go('IdentityVerified');
|
|
364
|
-
}
|
|
365
|
-
}, [requiredTier, go]);
|
|
366
|
-
|
|
367
|
-
// Handle MintingIdentity completion (wallet + DID created for L1, or verification submitted for L2/L3)
|
|
368
|
-
const handleMintingSuccess = useCallback(async (result?: { wallet?: string; didAddress?: string; credentialHash?: string }) => {
|
|
369
|
-
if (result?.wallet) {
|
|
370
|
-
setAuthState(prev => ({
|
|
371
|
-
...prev,
|
|
372
|
-
userAccount: {
|
|
373
|
-
...prev.userAccount,
|
|
374
|
-
user_id: prev.userAccount?.user_id || '',
|
|
375
|
-
wallet: result.wallet,
|
|
376
|
-
did_address: result.didAddress,
|
|
377
|
-
tier: 'layer_1'
|
|
378
|
-
}
|
|
379
|
-
}));
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (result?.credentialHash) {
|
|
383
|
-
setCredentialHash(result.credentialHash);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// REFRESH TOKEN: Get a fresh token that includes the new verified wallet and humanId
|
|
387
|
-
if (sessionToken && !previewMode) {
|
|
388
|
-
try {
|
|
389
|
-
const response = await fetch(`${finalApiBaseUrl}/api/casperid/refresh`, {
|
|
390
|
-
method: 'POST',
|
|
391
|
-
headers: { 'Content-Type': 'application/json' },
|
|
392
|
-
body: JSON.stringify({ token: sessionToken, apiKey })
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
if (response.ok) {
|
|
396
|
-
const data = await response.json();
|
|
397
|
-
if (data.success && data.token) {
|
|
398
|
-
console.log('[CasperID SDK] Token refreshed after verification');
|
|
399
|
-
setSessionToken(data.token);
|
|
400
|
-
setAuthState(prev => ({
|
|
401
|
-
...prev,
|
|
402
|
-
sessionToken: data.token,
|
|
403
|
-
businessToken: data.business_token || prev.businessToken
|
|
404
|
-
}));
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
} catch (err) {
|
|
408
|
-
console.warn('[CasperID SDK] Failed to refresh token after verification:', err);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Check tier requirements after minting
|
|
413
|
-
const currentTier = result?.wallet ? 'layer_1' : (authState.userAccount?.tier || 'layer_1');
|
|
414
|
-
proceedBasedOnTier(currentTier);
|
|
415
|
-
}, [authState.userAccount?.tier, proceedBasedOnTier, sessionToken, finalApiBaseUrl, previewMode]);
|
|
416
|
-
|
|
417
|
-
// Handle extension login success
|
|
418
|
-
const handleExtensionSuccess = useCallback((userData: any) => {
|
|
419
|
-
// Extension auth is complete - use token from extension if provided
|
|
420
|
-
const token = userData?.sessionToken || userData?.token;
|
|
421
|
-
const bToken = userData?.business_token || userData?.businessToken;
|
|
422
|
-
|
|
423
|
-
if (token) {
|
|
424
|
-
setSessionToken(token);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
if (bToken) {
|
|
428
|
-
setAuthState(prev => ({ ...prev, businessToken: bToken }));
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Skip to appropriate screen based on user's tier
|
|
432
|
-
go('SecurityUpgrade');
|
|
433
|
-
}, [go]);
|
|
434
|
-
|
|
435
|
-
// Handle SecurityUpgrade start
|
|
436
|
-
const handleSecurityUpgradeNext = useCallback(async () => {
|
|
437
|
-
// Start Layer 2 verification (face scan)
|
|
438
|
-
const requestId = await startVerification('layer_2');
|
|
439
|
-
if (requestId) {
|
|
440
|
-
go('FaceScan');
|
|
441
|
-
}
|
|
442
|
-
}, [startVerification, go]);
|
|
443
|
-
|
|
444
|
-
// Handle FaceScan completion
|
|
445
|
-
const handleFaceScanNext = useCallback(async (livenessVerified: boolean) => {
|
|
446
|
-
setKycState(prev => ({ ...prev, livenessVerified }));
|
|
447
|
-
|
|
448
|
-
// Check if we need Layer 3 (document verification)
|
|
449
|
-
if (requiredTier === 'L3') {
|
|
450
|
-
// Start Layer 3 verification
|
|
451
|
-
const requestId = await startVerification('layer_3');
|
|
452
|
-
if (requestId) {
|
|
453
|
-
go('VerifyIdentityChoice');
|
|
454
|
-
}
|
|
455
|
-
} else {
|
|
456
|
-
// Layer 2 complete, go to minting
|
|
457
|
-
go('MintingIdentity');
|
|
458
|
-
}
|
|
459
|
-
}, [requiredTier, startVerification, go]);
|
|
460
|
-
|
|
461
|
-
// Handle document type selection
|
|
462
|
-
const handleDocumentTypeSelected = useCallback((documentType: DocumentType) => {
|
|
463
|
-
setKycState(prev => ({ ...prev, documentType }));
|
|
464
|
-
go('DocumentScan');
|
|
465
|
-
}, [go]);
|
|
466
|
-
|
|
467
|
-
// Handle document scan completion
|
|
468
|
-
const handleDocumentScanNext = useCallback((extractedData: ExtractedDocumentData, docUploadId: string) => {
|
|
469
|
-
setKycState(prev => ({ ...prev, extractedData }));
|
|
470
|
-
setUploadId(docUploadId);
|
|
471
|
-
go('ReviewData');
|
|
472
|
-
}, [go]);
|
|
473
|
-
|
|
474
|
-
// Handle review data confirmation
|
|
475
|
-
const handleReviewDataNext = useCallback((confirmedData: ExtractedDocumentData) => {
|
|
476
|
-
setKycState(prev => ({ ...prev, extractedData: confirmedData }));
|
|
477
|
-
go('MintingIdentity');
|
|
478
|
-
}, [go]);
|
|
479
|
-
|
|
480
|
-
// Handle document retake
|
|
481
|
-
const handleDocumentRetake = useCallback(() => {
|
|
482
|
-
setKycState(prev => ({ ...prev, extractedData: undefined }));
|
|
483
|
-
setUploadId('');
|
|
484
|
-
go('DocumentScan');
|
|
485
|
-
}, [go]);
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
// Handle final success (after permissions approved)
|
|
489
|
-
const handleSuccess = useCallback(() => {
|
|
490
|
-
if (!sessionToken) {
|
|
491
|
-
onError?.(new Error('No valid session token available'));
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
onSuccess?.(sessionToken, {
|
|
495
|
-
businessToken: authState.businessToken
|
|
496
|
-
});
|
|
497
|
-
onClose?.();
|
|
498
|
-
}, [sessionToken, authState.businessToken, onSuccess, onClose, onError]);
|
|
499
|
-
|
|
500
|
-
// Handle errors
|
|
501
|
-
const handleError = useCallback((error: string) => {
|
|
502
|
-
onError?.(new Error(error));
|
|
503
|
-
}, [onError]);
|
|
504
|
-
|
|
505
|
-
// Login screen handlers
|
|
506
|
-
const handleLoginNext = useCallback((email: string, verificationId: string) => {
|
|
507
|
-
setOtpState({ email, verificationId });
|
|
508
|
-
setAuthState(prev => ({ ...prev, email }));
|
|
509
|
-
go('PinVerification');
|
|
510
|
-
}, [go]);
|
|
511
|
-
|
|
512
|
-
if (!isOpen && !previewMode) return null;
|
|
513
|
-
|
|
514
|
-
// Show loading state while fetching cloud theme (prevents flash of default colors)
|
|
515
|
-
if (isLoadingCloudTheme && !previewMode) {
|
|
516
|
-
return (
|
|
517
|
-
<div
|
|
518
|
-
className="fixed inset-0 z-[9999] flex items-center justify-center p-4"
|
|
519
|
-
style={{ backgroundColor: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(6px)' }}
|
|
520
|
-
>
|
|
521
|
-
<div className="w-12 h-12 border-3 border-white/20 border-t-white/80 rounded-full animate-spin" />
|
|
522
|
-
</div>
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Render screen content
|
|
527
|
-
const renderScreen = () => {
|
|
528
|
-
switch (screen) {
|
|
529
|
-
case 'AuthSelection':
|
|
530
|
-
return (
|
|
531
|
-
<AuthSelection
|
|
532
|
-
onNext={(method) => go(method === 'otp' ? 'Login' : 'SecurityUpgrade')}
|
|
533
|
-
onExtensionSuccess={handleExtensionSuccess}
|
|
534
|
-
onError={handleError}
|
|
535
|
-
language={language}
|
|
536
|
-
/>
|
|
537
|
-
);
|
|
538
|
-
case 'Login':
|
|
539
|
-
return (
|
|
540
|
-
<Login
|
|
541
|
-
onNext={handleLoginNext}
|
|
542
|
-
onPasskeyUser={handlePasskeyUser}
|
|
543
|
-
onError={handleError}
|
|
544
|
-
apiBaseUrl={finalApiBaseUrl}
|
|
545
|
-
previewMode={previewMode}
|
|
546
|
-
language={language}
|
|
547
|
-
/>
|
|
548
|
-
);
|
|
549
|
-
case 'PasskeyAuth':
|
|
550
|
-
return (
|
|
551
|
-
<PasskeyAuth
|
|
552
|
-
email={authState.email}
|
|
553
|
-
onSuccess={handlePasskeyAuthSuccess}
|
|
554
|
-
onError={handleError}
|
|
555
|
-
onFallbackToOTP={() => go('Login')}
|
|
556
|
-
apiBaseUrl={finalApiBaseUrl}
|
|
557
|
-
previewMode={previewMode}
|
|
558
|
-
language={language}
|
|
559
|
-
/>
|
|
560
|
-
);
|
|
561
|
-
case 'PasskeyRegister':
|
|
562
|
-
return (
|
|
563
|
-
<PasskeyRegister
|
|
564
|
-
email={authState.email || otpState.email}
|
|
565
|
-
sessionToken={sessionToken || ''}
|
|
566
|
-
onSuccess={handlePasskeyRegisterSuccess}
|
|
567
|
-
onError={handleError}
|
|
568
|
-
apiBaseUrl={finalApiBaseUrl}
|
|
569
|
-
previewMode={previewMode}
|
|
570
|
-
language={language}
|
|
571
|
-
/>
|
|
572
|
-
);
|
|
573
|
-
case 'PinVerification':
|
|
574
|
-
return (
|
|
575
|
-
<PinVerification
|
|
576
|
-
email={otpState.email}
|
|
577
|
-
verificationId={otpState.verificationId}
|
|
578
|
-
onNext={handleOTPSuccess}
|
|
579
|
-
onError={handleError}
|
|
580
|
-
apiBaseUrl={finalApiBaseUrl}
|
|
581
|
-
apiKey={apiKey}
|
|
582
|
-
previewMode={previewMode}
|
|
583
|
-
language={language}
|
|
584
|
-
/>
|
|
585
|
-
);
|
|
586
|
-
case 'SecurityUpgrade':
|
|
587
|
-
return (
|
|
588
|
-
<SecurityUpgrade
|
|
589
|
-
onNext={handleSecurityUpgradeNext}
|
|
590
|
-
onSkip={() => go('IdentityVerified')}
|
|
591
|
-
language={language}
|
|
592
|
-
/>
|
|
593
|
-
);
|
|
594
|
-
case 'FaceScan':
|
|
595
|
-
return (
|
|
596
|
-
<FaceScan
|
|
597
|
-
requestId={kycState.requestId}
|
|
598
|
-
onNext={handleFaceScanNext}
|
|
599
|
-
onError={handleError}
|
|
600
|
-
apiBaseUrl={finalApiBaseUrl}
|
|
601
|
-
previewMode={previewMode}
|
|
602
|
-
language={language}
|
|
603
|
-
/>
|
|
604
|
-
);
|
|
605
|
-
case 'VerifyIdentityChoice':
|
|
606
|
-
return (
|
|
607
|
-
<VerifyIdentityChoice
|
|
608
|
-
onNext={handleDocumentTypeSelected}
|
|
609
|
-
language={language}
|
|
610
|
-
/>
|
|
611
|
-
);
|
|
612
|
-
case 'DocumentScan':
|
|
613
|
-
return (
|
|
614
|
-
<DocumentScan
|
|
615
|
-
requestId={kycState.requestId}
|
|
616
|
-
documentType={kycState.documentType || 'passport'}
|
|
617
|
-
onNext={handleDocumentScanNext}
|
|
618
|
-
onError={handleError}
|
|
619
|
-
apiBaseUrl={finalApiBaseUrl}
|
|
620
|
-
previewMode={previewMode}
|
|
621
|
-
language={language}
|
|
622
|
-
/>
|
|
623
|
-
);
|
|
624
|
-
case 'ReviewData':
|
|
625
|
-
return (
|
|
626
|
-
<ReviewData
|
|
627
|
-
requestId={kycState.requestId}
|
|
628
|
-
documentType={kycState.documentType || 'passport'}
|
|
629
|
-
extractedData={kycState.extractedData || {}}
|
|
630
|
-
uploadId={uploadId}
|
|
631
|
-
onNext={handleReviewDataNext}
|
|
632
|
-
onRetake={handleDocumentRetake}
|
|
633
|
-
onError={handleError}
|
|
634
|
-
apiBaseUrl={finalApiBaseUrl}
|
|
635
|
-
previewMode={previewMode}
|
|
636
|
-
language={language}
|
|
637
|
-
/>
|
|
638
|
-
);
|
|
639
|
-
case 'MintingIdentity':
|
|
640
|
-
// Determine the tier based on current state
|
|
641
|
-
const mintingTier = kycState.requestId ? kycState.verificationTier : 'layer_1';
|
|
642
|
-
return (
|
|
643
|
-
<MintingIdentity
|
|
644
|
-
tier={mintingTier}
|
|
645
|
-
sessionToken={sessionToken || ''}
|
|
646
|
-
requestId={kycState.requestId}
|
|
647
|
-
onNext={handleMintingSuccess}
|
|
648
|
-
onError={handleError}
|
|
649
|
-
apiBaseUrl={finalApiBaseUrl}
|
|
650
|
-
previewMode={previewMode}
|
|
651
|
-
language={language}
|
|
652
|
-
/>
|
|
653
|
-
);
|
|
654
|
-
case 'IdentityVerified':
|
|
655
|
-
return <IdentityVerified onNext={() => go('PermissionsRequest')} language={language} />;
|
|
656
|
-
case 'PermissionsRequest':
|
|
657
|
-
return (
|
|
658
|
-
<PermissionsRequest
|
|
659
|
-
onApprove={handleSuccess}
|
|
660
|
-
onDeny={() => onClose?.()}
|
|
661
|
-
language={language}
|
|
662
|
-
appName={appMetadata?.name}
|
|
663
|
-
appLogo={logoUrl || appMetadata?.logo_url}
|
|
664
|
-
/>
|
|
665
|
-
);
|
|
666
|
-
default:
|
|
667
|
-
return null;
|
|
668
|
-
}
|
|
669
|
-
};
|
|
670
|
-
|
|
671
|
-
// In previewMode, render the card directly (no fixed overlay)
|
|
672
|
-
if (previewMode) {
|
|
673
|
-
return (
|
|
674
|
-
<>
|
|
675
|
-
<style key={styleKey}>{scopedStyles}</style>
|
|
676
|
-
<div id={`casperid-modal-${modalId}`} style={cssVars} className={`relative ${resolvedTheme}`}>
|
|
677
|
-
<GlassContainer className="backdrop-blur-3xl" theme={resolvedTheme}>
|
|
678
|
-
<Header
|
|
679
|
-
onClose={onClose}
|
|
680
|
-
showClose={screen !== 'AuthSelection'}
|
|
681
|
-
subtitle={screen === 'PermissionsRequest' ? 'PERMISSION REQUEST' : 'SECURE SDK'}
|
|
682
|
-
theme={resolvedTheme}
|
|
683
|
-
toggleTheme={toggleTheme}
|
|
684
|
-
/>
|
|
685
|
-
<div className="flex-1 flex flex-col relative overflow-hidden">
|
|
686
|
-
<AnimatePresence mode="wait">
|
|
687
|
-
<motion.div key={screen} className="flex-1 flex flex-col">
|
|
688
|
-
{renderScreen()}
|
|
689
|
-
</motion.div>
|
|
690
|
-
</AnimatePresence>
|
|
691
|
-
</div>
|
|
692
|
-
{FOOTER_SCREENS.includes(screen) && (
|
|
693
|
-
<Footer>
|
|
694
|
-
<p className="text-[11px] text-slate-500 font-medium">
|
|
695
|
-
By connecting, you agree to our{' '}
|
|
696
|
-
<a className="text-brand hover:underline ml-1" href={termsUrl} target="_blank" rel="noopener noreferrer">Terms of Service</a>
|
|
697
|
-
{' '}&{' '}
|
|
698
|
-
<a className="text-brand hover:underline ml-1" href={privacyUrl} target="_blank" rel="noopener noreferrer">Privacy Policy</a>
|
|
699
|
-
</p>
|
|
700
|
-
</Footer>
|
|
701
|
-
)}
|
|
702
|
-
</GlassContainer>
|
|
703
|
-
</div>
|
|
704
|
-
</>
|
|
705
|
-
);
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
return (
|
|
709
|
-
<>
|
|
710
|
-
<style key={styleKey}>{scopedStyles}</style>
|
|
711
|
-
{/* Overlay backdrop — sits on top of the host app */}
|
|
712
|
-
<div
|
|
713
|
-
className={`fixed inset-0 z-[9999] flex items-center justify-center ${finalLayout === 'fullscreen' ? 'p-0' : 'p-4'}`}
|
|
714
|
-
style={{ backgroundColor: finalLayout === 'fullscreen' ? 'transparent' : 'rgba(0,0,0,0.6)', backdropFilter: finalLayout === 'fullscreen' ? 'none' : 'blur(6px)' }}
|
|
715
|
-
onClick={(e) => e.target === e.currentTarget && onClose?.()}
|
|
716
|
-
>
|
|
717
|
-
{/* Ambient glows — inside the overlay, behind the card */}
|
|
718
|
-
{finalLayout !== 'fullscreen' && (
|
|
719
|
-
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
|
720
|
-
<div
|
|
721
|
-
className="absolute top-[-10%] left-[-10%] w-[60%] h-[40%] rounded-full blur-[120px]"
|
|
722
|
-
style={{ backgroundColor: `${primaryColor}1a` }}
|
|
723
|
-
/>
|
|
724
|
-
<div className="absolute bottom-[-5%] right-[-5%] w-[50%] h-[30%] bg-[#6DE8EC]/5 blur-[100px] rounded-full" />
|
|
725
|
-
</div>
|
|
726
|
-
)}
|
|
727
|
-
|
|
728
|
-
{/* The glass modal card */}
|
|
729
|
-
<motion.div
|
|
730
|
-
id={`casperid-modal-${modalId}`}
|
|
731
|
-
initial={finalLayout === 'fullscreen' ? { opacity: 0 } : { opacity: 0, scale: 0.95, y: 20 }}
|
|
732
|
-
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
733
|
-
exit={finalLayout === 'fullscreen' ? { opacity: 0 } : { opacity: 0, scale: 0.95, y: 20 }}
|
|
734
|
-
transition={{ type: 'spring', damping: 28, stiffness: 300 }}
|
|
735
|
-
style={cssVars}
|
|
736
|
-
className={`relative selection:bg-brand/30 ${resolvedTheme} ${finalLayout === 'fullscreen' ? 'w-full h-full' : ''}`}
|
|
737
|
-
>
|
|
738
|
-
<GlassContainer className={`backdrop-blur-3xl ${finalLayout === 'fullscreen' ? 'w-full h-full rounded-none' : ''}`} theme={resolvedTheme}>
|
|
739
|
-
<Header
|
|
740
|
-
onClose={onClose}
|
|
741
|
-
showClose={screen !== 'AuthSelection'}
|
|
742
|
-
subtitle={screen === 'PermissionsRequest' ? 'PERMISSION REQUEST' : 'SECURE SDK'}
|
|
743
|
-
theme={resolvedTheme}
|
|
744
|
-
toggleTheme={toggleTheme}
|
|
745
|
-
/>
|
|
746
|
-
|
|
747
|
-
<div className="flex-1 flex flex-col relative overflow-hidden">
|
|
748
|
-
<AnimatePresence mode="wait">
|
|
749
|
-
<motion.div key={screen} className="flex-1 flex flex-col">
|
|
750
|
-
{renderScreen()}
|
|
751
|
-
</motion.div>
|
|
752
|
-
</AnimatePresence>
|
|
753
|
-
</div>
|
|
754
|
-
|
|
755
|
-
{/* Footer with ToS links, shown on early screens */}
|
|
756
|
-
{FOOTER_SCREENS.includes(screen) && (
|
|
757
|
-
<Footer>
|
|
758
|
-
<p className="text-[11px] text-slate-500 font-medium">
|
|
759
|
-
By connecting, you agree to our{' '}
|
|
760
|
-
<a className="text-brand hover:underline ml-1" href={termsUrl} target="_blank" rel="noopener noreferrer">
|
|
761
|
-
Terms of Service
|
|
762
|
-
</a>{' '}
|
|
763
|
-
&{' '}
|
|
764
|
-
<a className="text-brand hover:underline ml-1" href={privacyUrl} target="_blank" rel="noopener noreferrer">
|
|
765
|
-
Privacy Policy
|
|
766
|
-
</a>
|
|
767
|
-
</p>
|
|
768
|
-
</Footer>
|
|
769
|
-
)}
|
|
770
|
-
</GlassContainer>
|
|
771
|
-
</motion.div>
|
|
772
|
-
</div>
|
|
773
|
-
</>
|
|
774
|
-
);
|
|
775
|
-
};
|
|
776
|
-
|
|
777
|
-
export default CasperIDModal;
|