@casperid/react 1.0.1 → 2.0.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.
@@ -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
- {' '}&amp;{' '}
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
- &amp;{' '}
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;