@proveanything/smartlinks-auth-ui 0.1.4 → 0.1.6

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/index.esm.js CHANGED
@@ -47,7 +47,7 @@ const AuthContainer = ({ children, theme = 'light', className = '', config, mini
47
47
  // - null or empty string: hide logo completely
48
48
  // - string URL: use custom logo
49
49
  const logoUrl = config?.branding?.logoUrl === undefined
50
- ? '/smartlinks-logo.png' // Default
50
+ ? 'https://smartlinks.app/smartlinks/landscape-medium.png' // Default Smartlinks logo
51
51
  : config?.branding?.logoUrl || null; // Custom or explicitly hidden
52
52
  const containerClass = minimal
53
53
  ? `auth-minimal auth-theme-${theme} ${className}`
@@ -11411,1092 +11411,1117 @@ const useAuth = () => {
11411
11411
  return context;
11412
11412
  };
11413
11413
 
11414
- const AccountManagement = ({ apiEndpoint, clientId, onProfileUpdated, onEmailChangeRequested, onPasswordChanged, onAccountDeleted, onError, theme = 'light', className = '', customization = {}, minimal = false, }) => {
11415
- const auth = useAuth();
11414
+ // Default Smartlinks Google OAuth Client ID (public - safe to expose)
11415
+ const DEFAULT_GOOGLE_CLIENT_ID = '696509063554-jdlbjl8vsjt7cr0vgkjkjf3ffnvi3a70.apps.googleusercontent.com';
11416
+ // Default auth UI configuration when no clientId is provided
11417
+ const DEFAULT_AUTH_CONFIG = {
11418
+ branding: {
11419
+ title: '', // No title by default (minimal)
11420
+ subtitle: 'Sign in to your account',
11421
+ logoUrl: 'https://smartlinks.app/smartlinks/landscape-medium.png',
11422
+ primaryColor: '#3B82F6',
11423
+ secondaryColor: '#1D4ED8',
11424
+ backgroundColor: '#e0f2fe',
11425
+ buttonStyle: 'rounded',
11426
+ fontFamily: 'Inter, sans-serif'
11427
+ },
11428
+ emailDisplayMode: 'button',
11429
+ googleOAuthFlow: 'oneTap'
11430
+ };
11431
+ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, enabledProviders = ['email', 'google', 'phone'], initialMode = 'login', redirectUrl, theme = 'light', className, customization, skipConfigFetch = false, minimal = false, }) => {
11432
+ const [mode, setMode] = useState(initialMode);
11416
11433
  const [loading, setLoading] = useState(false);
11417
- const [profile, setProfile] = useState(null);
11418
11434
  const [error, setError] = useState();
11419
- const [success, setSuccess] = useState();
11420
- // Track which section is being edited
11421
- const [editingSection, setEditingSection] = useState(null);
11422
- // Profile form state
11423
- const [displayName, setDisplayName] = useState('');
11424
- // Email change state
11425
- const [newEmail, setNewEmail] = useState('');
11426
- const [emailPassword, setEmailPassword] = useState('');
11427
- // Password change state
11428
- const [currentPassword, setCurrentPassword] = useState('');
11429
- const [newPassword, setNewPassword] = useState('');
11430
- const [confirmPassword, setConfirmPassword] = useState('');
11431
- // Phone change state (reuses existing sendPhoneCode flow)
11432
- const [newPhone, setNewPhone] = useState('');
11433
- const [phoneCode, setPhoneCode] = useState('');
11434
- const [phoneCodeSent, setPhoneCodeSent] = useState(false);
11435
- // Account deletion state
11436
- const [deletePassword, setDeletePassword] = useState('');
11437
- const [deleteConfirmText, setDeleteConfirmText] = useState('');
11438
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
11439
- const { showProfileSection = true, showEmailSection = true, showPasswordSection = true, showPhoneSection = true, showDeleteAccount = false, } = customization;
11440
- // Reinitialize Smartlinks SDK when apiEndpoint changes
11435
+ const [resetSuccess, setResetSuccess] = useState(false);
11436
+ const [authSuccess, setAuthSuccess] = useState(false);
11437
+ const [successMessage, setSuccessMessage] = useState();
11438
+ const [showResendVerification, setShowResendVerification] = useState(false);
11439
+ const [resendEmail, setResendEmail] = useState();
11440
+ const [showRequestNewReset, setShowRequestNewReset] = useState(false);
11441
+ const [resetRequestEmail, setResetRequestEmail] = useState();
11442
+ const [resetToken, setResetToken] = useState(); // Store the reset token from URL
11443
+ const [config, setConfig] = useState(null);
11444
+ const [configLoading, setConfigLoading] = useState(!skipConfigFetch);
11445
+ const [showEmailForm, setShowEmailForm] = useState(false); // Track if email form should be shown when emailDisplayMode is 'button'
11446
+ const api = new AuthAPI(apiEndpoint, clientId, clientName);
11447
+ const auth = useAuth();
11448
+ // Reinitialize Smartlinks SDK when apiEndpoint changes (for test/dev scenarios)
11449
+ // IMPORTANT: Preserve bearer token during reinitialization
11441
11450
  useEffect(() => {
11442
- if (apiEndpoint) {
11443
- smartlinks.initializeApi({
11444
- baseURL: apiEndpoint,
11445
- proxyMode: false,
11446
- ngrokSkipBrowserWarning: true,
11447
- });
11448
- }
11449
- }, [apiEndpoint]);
11450
- // Load user profile on mount
11451
+ const reinitializeWithToken = async () => {
11452
+ if (apiEndpoint) {
11453
+ // Get current token before reinitializing
11454
+ const currentToken = await auth.getToken();
11455
+ smartlinks.initializeApi({
11456
+ baseURL: apiEndpoint,
11457
+ proxyMode: false, // Direct API calls when custom endpoint is provided
11458
+ ngrokSkipBrowserWarning: true,
11459
+ });
11460
+ // Restore bearer token after reinitialization using auth.verifyToken
11461
+ if (currentToken) {
11462
+ smartlinks.auth.verifyToken(currentToken).catch(err => {
11463
+ console.warn('Failed to restore bearer token after reinit:', err);
11464
+ });
11465
+ }
11466
+ }
11467
+ };
11468
+ reinitializeWithToken();
11469
+ }, [apiEndpoint, auth]);
11470
+ // Get the effective redirect URL (use prop or default to current page)
11471
+ const getRedirectUrl = () => {
11472
+ if (redirectUrl)
11473
+ return redirectUrl;
11474
+ // Get the full current URL including hash routes
11475
+ // Remove any existing query parameters to avoid duplication
11476
+ const currentUrl = window.location.href.split('?')[0];
11477
+ return currentUrl;
11478
+ };
11479
+ // Fetch UI configuration
11451
11480
  useEffect(() => {
11452
- loadProfile();
11453
- }, [clientId]);
11454
- const loadProfile = async () => {
11455
- if (!auth.isAuthenticated) {
11456
- setError('You must be logged in to manage your account');
11481
+ if (skipConfigFetch) {
11482
+ setConfig(customization || {});
11457
11483
  return;
11458
11484
  }
11459
- setLoading(true);
11460
- setError(undefined);
11461
- try {
11462
- // TODO: Backend implementation required
11463
- // Endpoint: GET /api/v1/authkit/:clientId/account/profile
11464
- // SDK method: smartlinks.authKit.getProfile(clientId)
11465
- // Temporary mock data for UI testing
11466
- const profileData = {
11467
- uid: auth.user?.uid || '',
11468
- email: auth.user?.email,
11469
- displayName: auth.user?.displayName,
11470
- phoneNumber: auth.user?.phoneNumber,
11471
- photoURL: auth.user?.photoURL,
11472
- emailVerified: true,
11473
- accountData: auth.accountData || {},
11474
- };
11475
- setProfile(profileData);
11476
- setDisplayName(profileData.displayName || '');
11477
- }
11478
- catch (err) {
11479
- const errorMessage = err instanceof Error ? err.message : 'Failed to load profile';
11480
- setError(errorMessage);
11481
- onError?.(err instanceof Error ? err : new Error(errorMessage));
11482
- }
11483
- finally {
11484
- setLoading(false);
11485
- }
11486
- };
11487
- const handleUpdateProfile = async (e) => {
11488
- e.preventDefault();
11489
- setLoading(true);
11490
- setError(undefined);
11491
- setSuccess(undefined);
11492
- try {
11493
- // TODO: Backend implementation required
11494
- // Endpoint: POST /api/v1/authkit/:clientId/account/update-profile
11495
- // SDK method: smartlinks.authKit.updateProfile(clientId, updateData)
11496
- setError('Backend API not yet implemented. See console for required endpoint.');
11497
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-profile');
11498
- console.log('Update data:', { displayName });
11499
- // Uncomment when backend is ready:
11500
- // const updateData: ProfileUpdateData = {
11501
- // displayName: displayName || undefined,
11502
- // photoURL: photoURL || undefined,
11503
- // };
11504
- // const updatedProfile = await smartlinks.authKit.updateProfile(clientId, updateData);
11505
- // setProfile(updatedProfile);
11506
- // setSuccess('Profile updated successfully!');
11507
- // setEditingSection(null);
11508
- // onProfileUpdated?.(updatedProfile);
11509
- }
11510
- catch (err) {
11511
- const errorMessage = err instanceof Error ? err.message : 'Failed to update profile';
11512
- setError(errorMessage);
11513
- onError?.(err instanceof Error ? err : new Error(errorMessage));
11485
+ const fetchConfig = async () => {
11486
+ // If no clientId provided, use default config immediately without API call
11487
+ if (!clientId) {
11488
+ const defaultConfig = {
11489
+ ...DEFAULT_AUTH_CONFIG,
11490
+ enabledProviders: enabledProviders || ['email', 'google', 'phone']
11491
+ };
11492
+ setConfig({ ...defaultConfig, ...customization });
11493
+ setConfigLoading(false);
11494
+ return;
11495
+ }
11496
+ try {
11497
+ // Check localStorage cache first
11498
+ const cacheKey = `auth_ui_config_${clientId}`;
11499
+ const cached = localStorage.getItem(cacheKey);
11500
+ if (cached) {
11501
+ const { config: cachedConfig, timestamp } = JSON.parse(cached);
11502
+ const age = Date.now() - timestamp;
11503
+ // Use cache if less than 1 hour old
11504
+ if (age < 3600000) {
11505
+ setConfig({ ...cachedConfig, ...customization });
11506
+ setConfigLoading(false);
11507
+ // Fetch in background to update cache
11508
+ api.fetchConfig().then(freshConfig => {
11509
+ localStorage.setItem(cacheKey, JSON.stringify({
11510
+ config: freshConfig,
11511
+ timestamp: Date.now()
11512
+ }));
11513
+ });
11514
+ return;
11515
+ }
11516
+ }
11517
+ // Fetch from API
11518
+ const fetchedConfig = await api.fetchConfig();
11519
+ // Merge with customization props (props take precedence)
11520
+ const mergedConfig = { ...fetchedConfig, ...customization };
11521
+ setConfig(mergedConfig);
11522
+ // Cache the fetched config
11523
+ localStorage.setItem(cacheKey, JSON.stringify({
11524
+ config: fetchedConfig,
11525
+ timestamp: Date.now()
11526
+ }));
11527
+ }
11528
+ catch (err) {
11529
+ console.error('Failed to fetch config:', err);
11530
+ setConfig(customization || {});
11531
+ }
11532
+ finally {
11533
+ setConfigLoading(false);
11534
+ }
11535
+ };
11536
+ fetchConfig();
11537
+ }, [apiEndpoint, clientId, customization, skipConfigFetch]);
11538
+ // Reset showEmailForm when mode changes away from login/register
11539
+ useEffect(() => {
11540
+ if (mode !== 'login' && mode !== 'register') {
11541
+ setShowEmailForm(false);
11514
11542
  }
11515
- finally {
11516
- setLoading(false);
11543
+ }, [mode]);
11544
+ // Handle URL-based auth flows (email verification, password reset)
11545
+ useEffect(() => {
11546
+ // Helper to get URL parameters from either hash or search
11547
+ const getUrlParams = () => {
11548
+ // First check if there are params in the hash (for hash routing)
11549
+ const hash = window.location.hash;
11550
+ const hashQueryIndex = hash.indexOf('?');
11551
+ if (hashQueryIndex !== -1) {
11552
+ // Extract query string from hash (e.g., #/test?mode=verifyEmail&token=abc)
11553
+ const hashQuery = hash.substring(hashQueryIndex + 1);
11554
+ return new URLSearchParams(hashQuery);
11555
+ }
11556
+ // Fall back to regular search params (for non-hash routing)
11557
+ return new URLSearchParams(window.location.search);
11558
+ };
11559
+ const params = getUrlParams();
11560
+ const urlMode = params.get('mode');
11561
+ const token = params.get('token');
11562
+ console.log('URL params detected:', { urlMode, token, hash: window.location.hash, search: window.location.search });
11563
+ if (urlMode && token) {
11564
+ handleURLBasedAuth(urlMode, token);
11517
11565
  }
11518
- };
11519
- const cancelEdit = () => {
11520
- setEditingSection(null);
11521
- setDisplayName(profile?.displayName || '');
11522
- setNewEmail('');
11523
- setEmailPassword('');
11524
- setCurrentPassword('');
11525
- setNewPassword('');
11526
- setConfirmPassword('');
11527
- setNewPhone('');
11528
- setPhoneCode('');
11529
- setPhoneCodeSent(false);
11530
- setError(undefined);
11531
- setSuccess(undefined);
11532
- };
11533
- const handleChangeEmail = async (e) => {
11534
- e.preventDefault();
11566
+ }, []);
11567
+ const handleURLBasedAuth = async (urlMode, token) => {
11535
11568
  setLoading(true);
11536
11569
  setError(undefined);
11537
- setSuccess(undefined);
11538
11570
  try {
11539
- // TODO: Backend implementation required
11540
- // Endpoint: POST /api/v1/authkit/:clientId/account/change-email
11541
- // SDK method: smartlinks.authKit.changeEmail(clientId, newEmail, password)
11542
- // Note: No verification flow for now - direct email update
11543
- setError('Backend API not yet implemented. See console for required endpoint.');
11544
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-email');
11545
- console.log('Data:', { newEmail });
11546
- // Uncomment when backend is ready:
11547
- // await smartlinks.authKit.changeEmail(clientId, newEmail, emailPassword);
11548
- // setSuccess('Email changed successfully!');
11549
- // setEditingSection(null);
11550
- // setNewEmail('');
11551
- // setEmailPassword('');
11552
- // onEmailChangeRequested?.();
11553
- // await loadProfile(); // Reload to show new email
11554
- }
11555
- catch (err) {
11556
- const errorMessage = err instanceof Error ? err.message : 'Failed to change email';
11557
- setError(errorMessage);
11558
- onError?.(err instanceof Error ? err : new Error(errorMessage));
11571
+ if (urlMode === 'verifyEmail') {
11572
+ console.log('Verifying email with token:', token);
11573
+ const response = await api.verifyEmailWithToken(token);
11574
+ // Get email verification mode from response or config
11575
+ const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
11576
+ if ((verificationMode === 'verify-then-auto-login' || verificationMode === 'immediate') && response.token) {
11577
+ // Auto-login modes: Log the user in immediately if token is provided
11578
+ auth.login(response.token, response.user, response.accountData);
11579
+ setAuthSuccess(true);
11580
+ setSuccessMessage('Email verified successfully! You are now logged in.');
11581
+ onAuthSuccess(response.token, response.user, response.accountData);
11582
+ // Clear the URL parameters
11583
+ const cleanUrl = window.location.href.split('?')[0];
11584
+ window.history.replaceState({}, document.title, cleanUrl);
11585
+ // Redirect after a brief delay to show success message
11586
+ if (redirectUrl) {
11587
+ setTimeout(() => {
11588
+ window.location.href = redirectUrl;
11589
+ }, 2000);
11590
+ }
11591
+ }
11592
+ else {
11593
+ // verify-then-manual-login mode or no token: Show success but require manual login
11594
+ setAuthSuccess(true);
11595
+ setSuccessMessage('Email verified successfully! Please log in with your credentials.');
11596
+ // Clear the URL parameters
11597
+ const cleanUrl = window.location.href.split('?')[0];
11598
+ window.history.replaceState({}, document.title, cleanUrl);
11599
+ // Switch back to login mode after a delay
11600
+ setTimeout(() => {
11601
+ setAuthSuccess(false);
11602
+ setMode('login');
11603
+ }, 3000);
11604
+ }
11605
+ }
11606
+ else if (urlMode === 'resetPassword') {
11607
+ console.log('Verifying reset token:', token);
11608
+ // Verify token is valid, then show password reset form
11609
+ await api.verifyResetToken(token);
11610
+ setResetToken(token); // Store token for use in password reset
11611
+ setMode('reset-password');
11612
+ // Clear the URL parameters
11613
+ const cleanUrl = window.location.href.split('?')[0];
11614
+ window.history.replaceState({}, document.title, cleanUrl);
11615
+ }
11616
+ else if (urlMode === 'magicLink') {
11617
+ console.log('Verifying magic link token:', token);
11618
+ const response = await api.verifyMagicLink(token);
11619
+ // Auto-login with magic link if token is provided
11620
+ if (response.token) {
11621
+ auth.login(response.token, response.user, response.accountData);
11622
+ setAuthSuccess(true);
11623
+ setSuccessMessage('Magic link verified! You are now logged in.');
11624
+ onAuthSuccess(response.token, response.user, response.accountData);
11625
+ // Clear the URL parameters
11626
+ const cleanUrl = window.location.href.split('?')[0];
11627
+ window.history.replaceState({}, document.title, cleanUrl);
11628
+ // Redirect after a brief delay to show success message
11629
+ if (redirectUrl) {
11630
+ setTimeout(() => {
11631
+ window.location.href = redirectUrl;
11632
+ }, 2000);
11633
+ }
11634
+ }
11635
+ else {
11636
+ throw new Error('Authentication failed - no token received');
11637
+ }
11638
+ }
11639
+ }
11640
+ catch (err) {
11641
+ console.error('URL-based auth error:', err);
11642
+ const errorMessage = err instanceof Error ? err.message : 'An error occurred';
11643
+ // If it's an email verification error (expired/invalid token), show resend option
11644
+ if (urlMode === 'verifyEmail') {
11645
+ setError(`${errorMessage} - Please enter your email below to receive a new verification link.`);
11646
+ setShowResendVerification(true);
11647
+ setMode('login'); // Show the login form UI
11648
+ // Clear the URL parameters
11649
+ const cleanUrl = window.location.href.split('?')[0];
11650
+ window.history.replaceState({}, document.title, cleanUrl);
11651
+ }
11652
+ else if (urlMode === 'resetPassword') {
11653
+ // If password reset token is invalid/expired, show request new reset link option
11654
+ setError(`${errorMessage} - Please enter your email below to receive a new password reset link.`);
11655
+ setShowRequestNewReset(true);
11656
+ setMode('login');
11657
+ // Clear the URL parameters
11658
+ const cleanUrl = window.location.href.split('?')[0];
11659
+ window.history.replaceState({}, document.title, cleanUrl);
11660
+ }
11661
+ else if (urlMode === 'magicLink') {
11662
+ // If magic link is invalid/expired
11663
+ setError(`${errorMessage} - Please request a new magic link below.`);
11664
+ setMode('magic-link');
11665
+ // Clear the URL parameters
11666
+ const cleanUrl = window.location.href.split('?')[0];
11667
+ window.history.replaceState({}, document.title, cleanUrl);
11668
+ }
11669
+ else {
11670
+ setError(errorMessage);
11671
+ }
11672
+ onAuthError?.(err instanceof Error ? err : new Error('An error occurred'));
11559
11673
  }
11560
11674
  finally {
11561
11675
  setLoading(false);
11562
11676
  }
11563
11677
  };
11564
- const handleChangePassword = async (e) => {
11565
- e.preventDefault();
11566
- if (newPassword !== confirmPassword) {
11567
- setError('New passwords do not match');
11568
- return;
11569
- }
11570
- if (newPassword.length < 6) {
11571
- setError('Password must be at least 6 characters');
11572
- return;
11573
- }
11678
+ const handleEmailAuth = async (data) => {
11574
11679
  setLoading(true);
11575
11680
  setError(undefined);
11576
- setSuccess(undefined);
11681
+ setAuthSuccess(false);
11577
11682
  try {
11578
- // TODO: Backend implementation required
11579
- // Endpoint: POST /api/v1/authkit/:clientId/account/change-password
11580
- // SDK method: smartlinks.authKit.changePassword(clientId, currentPassword, newPassword)
11581
- setError('Backend API not yet implemented. See console for required endpoint.');
11582
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-password');
11583
- console.log('Data: currentPassword and newPassword provided');
11584
- // Uncomment when backend is ready:
11585
- // await smartlinks.authKit.changePassword(clientId, currentPassword, newPassword);
11586
- // setSuccess('Password changed successfully!');
11587
- // setEditingSection(null);
11588
- // setCurrentPassword('');
11589
- // setNewPassword('');
11590
- // setConfirmPassword('');
11591
- // onPasswordChanged?.();
11683
+ const response = mode === 'login'
11684
+ ? await api.login(data.email, data.password)
11685
+ : await api.register({
11686
+ ...data,
11687
+ accountData: mode === 'register' ? accountData : undefined,
11688
+ redirectUrl: getRedirectUrl(), // Include redirect URL for email verification
11689
+ });
11690
+ // Get email verification mode from response or config (default: verify-then-auto-login)
11691
+ const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
11692
+ const gracePeriodHours = config?.emailVerification?.gracePeriodHours || 24;
11693
+ if (mode === 'register') {
11694
+ // Handle different verification modes
11695
+ if (verificationMode === 'immediate' && response.token) {
11696
+ // Immediate mode: Log in right away if token is provided
11697
+ auth.login(response.token, response.user, response.accountData);
11698
+ setAuthSuccess(true);
11699
+ const deadline = response.emailVerificationDeadline
11700
+ ? new Date(response.emailVerificationDeadline).toLocaleString()
11701
+ : `${gracePeriodHours} hours`;
11702
+ setSuccessMessage(`Account created! You're logged in now. Please verify your email by ${deadline} to keep your account active.`);
11703
+ if (response.token) {
11704
+ onAuthSuccess(response.token, response.user, response.accountData);
11705
+ }
11706
+ if (redirectUrl) {
11707
+ setTimeout(() => {
11708
+ window.location.href = redirectUrl;
11709
+ }, 2000);
11710
+ }
11711
+ }
11712
+ else if (verificationMode === 'verify-then-auto-login') {
11713
+ // Verify-then-auto-login mode: Don't log in yet, but will auto-login after email verification
11714
+ setAuthSuccess(true);
11715
+ setSuccessMessage('Account created! Please check your email and click the verification link to complete your registration.');
11716
+ }
11717
+ else {
11718
+ // verify-then-manual-login mode: Traditional flow
11719
+ setAuthSuccess(true);
11720
+ setSuccessMessage('Account created successfully! Please check your email to verify your account, then log in.');
11721
+ }
11722
+ }
11723
+ else {
11724
+ // Login mode - always log in if token is provided
11725
+ if (response.token) {
11726
+ // Check for account lock or verification requirements
11727
+ if (response.accountLocked) {
11728
+ throw new Error('Your account has been locked due to unverified email. Please check your email or request a new verification link.');
11729
+ }
11730
+ if (response.requiresEmailVerification) {
11731
+ throw new Error('Please verify your email before logging in. Check your inbox for the verification link.');
11732
+ }
11733
+ auth.login(response.token, response.user, response.accountData);
11734
+ setAuthSuccess(true);
11735
+ setSuccessMessage('Login successful!');
11736
+ onAuthSuccess(response.token, response.user, response.accountData);
11737
+ if (redirectUrl) {
11738
+ setTimeout(() => {
11739
+ window.location.href = redirectUrl;
11740
+ }, 2000);
11741
+ }
11742
+ }
11743
+ else {
11744
+ throw new Error('Authentication failed - please verify your email before logging in.');
11745
+ }
11746
+ }
11592
11747
  }
11593
11748
  catch (err) {
11594
- const errorMessage = err instanceof Error ? err.message : 'Failed to change password';
11595
- setError(errorMessage);
11596
- onError?.(err instanceof Error ? err : new Error(errorMessage));
11749
+ const errorMessage = err instanceof Error ? err.message : 'Authentication failed';
11750
+ // Check if error is about email already registered
11751
+ if (mode === 'register' && errorMessage.toLowerCase().includes('already') && errorMessage.toLowerCase().includes('email')) {
11752
+ setShowResendVerification(true);
11753
+ setResendEmail(data.email);
11754
+ setError('This email is already registered. If you didn\'t receive the verification email, you can resend it below.');
11755
+ }
11756
+ else {
11757
+ setError(errorMessage);
11758
+ }
11759
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11597
11760
  }
11598
11761
  finally {
11599
11762
  setLoading(false);
11600
11763
  }
11601
11764
  };
11602
- const handleSendPhoneCode = async () => {
11765
+ const handleResendVerification = async () => {
11766
+ if (!resendEmail)
11767
+ return;
11603
11768
  setLoading(true);
11604
11769
  setError(undefined);
11605
11770
  try {
11606
- await smartlinks.authKit.sendPhoneCode(clientId, newPhone);
11607
- setPhoneCodeSent(true);
11608
- setSuccess('Verification code sent to your phone');
11771
+ // For resend, we need the userId. If we don't have it, we need to handle this differently
11772
+ // The backend should ideally handle this case
11773
+ await api.resendVerification('unknown', resendEmail, getRedirectUrl());
11774
+ setAuthSuccess(true);
11775
+ setSuccessMessage('Verification email sent! Please check your inbox.');
11776
+ setShowResendVerification(false);
11609
11777
  }
11610
11778
  catch (err) {
11611
- const errorMessage = err instanceof Error ? err.message : 'Failed to send verification code';
11779
+ const errorMessage = err instanceof Error ? err.message : 'Failed to resend verification email';
11612
11780
  setError(errorMessage);
11613
- onError?.(err instanceof Error ? err : new Error(errorMessage));
11781
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11614
11782
  }
11615
11783
  finally {
11616
11784
  setLoading(false);
11617
11785
  }
11618
11786
  };
11619
- const handleUpdatePhone = async (e) => {
11620
- e.preventDefault();
11787
+ const handleRequestNewReset = async () => {
11788
+ if (!resetRequestEmail)
11789
+ return;
11621
11790
  setLoading(true);
11622
11791
  setError(undefined);
11623
- setSuccess(undefined);
11624
11792
  try {
11625
- // TODO: Backend implementation required
11626
- // Endpoint: POST /api/v1/authkit/:clientId/account/update-phone
11627
- // SDK method: smartlinks.authKit.updatePhone(clientId, phoneNumber, verificationCode)
11628
- setError('Backend API not yet implemented. See console for required endpoint.');
11629
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-phone');
11630
- console.log('Data:', { phoneNumber: newPhone, code: phoneCode });
11631
- // Uncomment when backend is ready:
11632
- // await smartlinks.authKit.updatePhone(clientId, newPhone, phoneCode);
11633
- // setSuccess('Phone number updated successfully!');
11634
- // setEditingSection(null);
11635
- // setNewPhone('');
11636
- // setPhoneCode('');
11637
- // setPhoneCodeSent(false);
11638
- // await loadProfile();
11793
+ await api.requestPasswordReset(resetRequestEmail, getRedirectUrl());
11794
+ setAuthSuccess(true);
11795
+ setSuccessMessage('Password reset email sent! Please check your inbox.');
11796
+ setShowRequestNewReset(false);
11797
+ setResetRequestEmail('');
11639
11798
  }
11640
11799
  catch (err) {
11641
- const errorMessage = err instanceof Error ? err.message : 'Failed to update phone number';
11800
+ const errorMessage = err instanceof Error ? err.message : 'Failed to send password reset email';
11642
11801
  setError(errorMessage);
11643
- onError?.(err instanceof Error ? err : new Error(errorMessage));
11802
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11644
11803
  }
11645
11804
  finally {
11646
11805
  setLoading(false);
11647
11806
  }
11648
11807
  };
11649
- const handleDeleteAccount = async () => {
11650
- if (deleteConfirmText !== 'DELETE') {
11651
- setError('Please type DELETE to confirm account deletion');
11652
- return;
11808
+ const handleGoogleLogin = async () => {
11809
+ // Use custom client ID from config, or fall back to default Smartlinks client ID
11810
+ const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
11811
+ // Determine OAuth flow: default to 'oneTap' for better UX, but allow 'popup' for iframe compatibility
11812
+ const oauthFlow = config?.googleOAuthFlow || 'oneTap';
11813
+ setLoading(true);
11814
+ setError(undefined);
11815
+ try {
11816
+ const google = window.google;
11817
+ if (!google) {
11818
+ throw new Error('Google Identity Services not loaded. Please check your internet connection.');
11819
+ }
11820
+ if (oauthFlow === 'popup') {
11821
+ // Use OAuth2 popup flow (works in iframes but requires popup permission)
11822
+ if (!google.accounts.oauth2) {
11823
+ throw new Error('Google OAuth2 not available');
11824
+ }
11825
+ const client = google.accounts.oauth2.initTokenClient({
11826
+ client_id: googleClientId,
11827
+ scope: 'openid email profile',
11828
+ callback: async (response) => {
11829
+ try {
11830
+ if (response.error) {
11831
+ throw new Error(response.error_description || response.error);
11832
+ }
11833
+ const accessToken = response.access_token;
11834
+ // Send access token to backend
11835
+ const authResponse = await api.loginWithGoogle(accessToken);
11836
+ if (authResponse.token) {
11837
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData);
11838
+ setAuthSuccess(true);
11839
+ setSuccessMessage('Google login successful!');
11840
+ onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
11841
+ }
11842
+ else {
11843
+ throw new Error('Authentication failed - no token received');
11844
+ }
11845
+ if (redirectUrl) {
11846
+ setTimeout(() => {
11847
+ window.location.href = redirectUrl;
11848
+ }, 2000);
11849
+ }
11850
+ setLoading(false);
11851
+ }
11852
+ catch (err) {
11853
+ const errorMessage = err instanceof Error ? err.message : 'Google login failed';
11854
+ setError(errorMessage);
11855
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11856
+ setLoading(false);
11857
+ }
11858
+ },
11859
+ });
11860
+ client.requestAccessToken();
11861
+ }
11862
+ else {
11863
+ // Use One Tap / Sign-In button flow (smoother UX but doesn't work in iframes)
11864
+ google.accounts.id.initialize({
11865
+ client_id: googleClientId,
11866
+ callback: async (response) => {
11867
+ try {
11868
+ const idToken = response.credential;
11869
+ const authResponse = await api.loginWithGoogle(idToken);
11870
+ if (authResponse.token) {
11871
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData);
11872
+ setAuthSuccess(true);
11873
+ setSuccessMessage('Google login successful!');
11874
+ onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
11875
+ }
11876
+ else {
11877
+ throw new Error('Authentication failed - no token received');
11878
+ }
11879
+ if (redirectUrl) {
11880
+ setTimeout(() => {
11881
+ window.location.href = redirectUrl;
11882
+ }, 2000);
11883
+ }
11884
+ setLoading(false);
11885
+ }
11886
+ catch (err) {
11887
+ const errorMessage = err instanceof Error ? err.message : 'Google login failed';
11888
+ setError(errorMessage);
11889
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11890
+ setLoading(false);
11891
+ }
11892
+ },
11893
+ auto_select: false,
11894
+ cancel_on_tap_outside: true,
11895
+ });
11896
+ google.accounts.id.prompt((notification) => {
11897
+ if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
11898
+ setLoading(false);
11899
+ }
11900
+ });
11901
+ }
11653
11902
  }
11654
- if (!deletePassword) {
11655
- setError('Password is required');
11656
- return;
11903
+ catch (err) {
11904
+ const errorMessage = err instanceof Error ? err.message : 'Google login failed';
11905
+ setError(errorMessage);
11906
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11907
+ setLoading(false);
11657
11908
  }
11909
+ };
11910
+ const handlePhoneAuth = async (phoneNumber, verificationCode) => {
11658
11911
  setLoading(true);
11659
11912
  setError(undefined);
11660
11913
  try {
11661
- // TODO: Backend implementation required
11662
- // Endpoint: DELETE /api/v1/authkit/:clientId/account/delete
11663
- // SDK method: smartlinks.authKit.deleteAccount(clientId, password, confirmText)
11664
- // Note: This performs a SOFT DELETE (marks as deleted, obfuscates email)
11665
- setError('Backend API not yet implemented. See console for required endpoint.');
11666
- console.log('Required API endpoint: DELETE /api/v1/authkit/:clientId/account/delete');
11667
- console.log('Data: password and confirmText="DELETE" provided');
11668
- console.log('Note: Backend should soft delete (mark deleted, obfuscate email, disable account)');
11669
- // Uncomment when backend is ready:
11670
- // await smartlinks.authKit.deleteAccount(clientId, deletePassword, deleteConfirmText);
11671
- // setSuccess('Account deleted successfully');
11672
- // onAccountDeleted?.();
11673
- // await auth.logout();
11914
+ if (!verificationCode) {
11915
+ // Send verification code via Twilio Verify Service
11916
+ await api.sendPhoneCode(phoneNumber);
11917
+ // Twilio Verify Service tracks the verification by phone number
11918
+ // No need to store verificationId
11919
+ }
11920
+ else {
11921
+ // Verify code - Twilio identifies the verification by phone number
11922
+ const response = await api.verifyPhoneCode(phoneNumber, verificationCode);
11923
+ // Update auth context with account data if token is provided
11924
+ if (response.token) {
11925
+ auth.login(response.token, response.user, response.accountData);
11926
+ onAuthSuccess(response.token, response.user, response.accountData);
11927
+ if (redirectUrl) {
11928
+ window.location.href = redirectUrl;
11929
+ }
11930
+ }
11931
+ else {
11932
+ throw new Error('Authentication failed - no token received');
11933
+ }
11934
+ }
11674
11935
  }
11675
11936
  catch (err) {
11676
- const errorMessage = err instanceof Error ? err.message : 'Failed to delete account';
11937
+ const errorMessage = err instanceof Error ? err.message : 'Phone authentication failed';
11677
11938
  setError(errorMessage);
11678
- onError?.(err instanceof Error ? err : new Error(errorMessage));
11939
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11679
11940
  }
11680
11941
  finally {
11681
11942
  setLoading(false);
11682
11943
  }
11683
11944
  };
11684
- if (!auth.isAuthenticated) {
11685
- return (jsx("div", { className: `account-management ${className}`, children: jsx("p", { className: "text-muted-foreground", children: "Please log in to manage your account" }) }));
11686
- }
11687
- if (loading && !profile) {
11688
- return (jsx("div", { className: `account-management ${className}`, children: jsx("p", { className: "text-muted-foreground", children: "Loading..." }) }));
11689
- }
11690
- return (jsxs("div", { className: `account-management ${className}`, style: { maxWidth: '600px' }, children: [error && (jsx("div", { className: "auth-error", role: "alert", children: error })), success && (jsx("div", { className: "auth-success", role: "alert", children: success })), showProfileSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Display Name" }), jsx("div", { className: "field-value", children: profile?.displayName || 'Not set' })] }), editingSection !== 'profile' && (jsx("button", { type: "button", onClick: () => setEditingSection('profile'), className: "auth-button button-secondary", children: "Change" }))] }), editingSection === 'profile' && (jsxs("form", { onSubmit: handleUpdateProfile, className: "edit-form", children: [jsx("div", { className: "form-group", children: jsx("input", { id: "displayName", type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "Your display name", className: "auth-input" }) }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showEmailSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Email Address" }), jsxs("div", { className: "field-value", children: [profile?.email || 'Not set', profile?.emailVerified && (jsx("span", { className: "verification-badge verified", children: "Verified" })), profile?.email && !profile?.emailVerified && (jsx("span", { className: "verification-badge unverified", children: "Unverified" }))] })] }), editingSection !== 'email' && (jsx("button", { type: "button", onClick: () => setEditingSection('email'), className: "auth-button button-secondary", children: "Change Email" }))] }), editingSection === 'email' && (jsxs("form", { onSubmit: handleChangeEmail, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newEmail", children: "New Email" }), jsx("input", { id: "newEmail", type: "email", value: newEmail, onChange: (e) => setNewEmail(e.target.value), placeholder: "new.email@example.com", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "emailPassword", children: "Confirm Password" }), jsx("input", { id: "emailPassword", type: "password", value: emailPassword, onChange: (e) => setEmailPassword(e.target.value), placeholder: "Enter your password", className: "auth-input", required: true })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Email' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPasswordSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Password" }), jsx("div", { className: "field-value", children: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] }), editingSection !== 'password' && (jsx("button", { type: "button", onClick: () => setEditingSection('password'), className: "auth-button button-secondary", children: "Change Password" }))] }), editingSection === 'password' && (jsxs("form", { onSubmit: handleChangePassword, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "currentPassword", children: "Current Password" }), jsx("input", { id: "currentPassword", type: "password", value: currentPassword, onChange: (e) => setCurrentPassword(e.target.value), placeholder: "Enter current password", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPassword", children: "New Password" }), jsx("input", { id: "newPassword", type: "password", value: newPassword, onChange: (e) => setNewPassword(e.target.value), placeholder: "Enter new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "confirmPassword", children: "Confirm New Password" }), jsx("input", { id: "confirmPassword", type: "password", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), placeholder: "Confirm new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Password' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPhoneSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Phone Number" }), jsx("div", { className: "field-value", children: profile?.phoneNumber || 'Not set' })] }), editingSection !== 'phone' && (jsx("button", { type: "button", onClick: () => setEditingSection('phone'), className: "auth-button button-secondary", children: "Change Phone" }))] }), editingSection === 'phone' && (jsxs("form", { onSubmit: handleUpdatePhone, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPhone", children: "New Phone Number" }), jsx("input", { id: "newPhone", type: "tel", value: newPhone, onChange: (e) => setNewPhone(e.target.value), placeholder: "+1234567890", className: "auth-input", required: true })] }), !phoneCodeSent ? (jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleSendPhoneCode, className: "auth-button", disabled: loading || !newPhone, children: loading ? 'Sending...' : 'Send Code' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })) : (jsxs(Fragment, { children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "phoneCode", children: "Verification Code" }), jsx("input", { id: "phoneCode", type: "text", value: phoneCode, onChange: (e) => setPhoneCode(e.target.value), placeholder: "Enter 6-digit code", className: "auth-input", required: true, maxLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Verifying...' : 'Verify & Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] })), showDeleteAccount && (jsxs("section", { className: "account-section danger-zone", children: [jsx("h3", { className: "section-title text-danger", children: "Danger Zone" }), !showDeleteConfirm ? (jsx("button", { type: "button", onClick: () => setShowDeleteConfirm(true), className: "auth-button button-danger", children: "Delete Account" })) : (jsxs("div", { className: "delete-confirm", children: [jsx("p", { className: "warning-text", children: "\u26A0\uFE0F This action cannot be undone. This will permanently delete your account and all associated data." }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deletePassword", children: "Confirm Password" }), jsx("input", { id: "deletePassword", type: "password", value: deletePassword, onChange: (e) => setDeletePassword(e.target.value), placeholder: "Enter your password", className: "auth-input" })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deleteConfirm", children: "Type DELETE to confirm" }), jsx("input", { id: "deleteConfirm", type: "text", value: deleteConfirmText, onChange: (e) => setDeleteConfirmText(e.target.value), placeholder: "DELETE", className: "auth-input" })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleDeleteAccount, className: "auth-button button-danger", disabled: loading, children: loading ? 'Deleting...' : 'Permanently Delete Account' }), jsx("button", { type: "button", onClick: () => {
11691
- setShowDeleteConfirm(false);
11692
- setDeletePassword('');
11693
- setDeleteConfirmText('');
11694
- }, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] }));
11695
- };
11696
-
11697
- const SmartlinksClaimUI = ({ apiEndpoint, clientId, clientName, collectionId, productId, proofId, onClaimSuccess, onClaimError, additionalFields = [], theme = 'light', className = '', minimal = false, customization = {}, }) => {
11698
- const auth = useAuth();
11699
- const [claimStep, setClaimStep] = useState(auth.isAuthenticated ? 'questions' : 'auth');
11700
- const [claimData, setClaimData] = useState({});
11701
- const [error, setError] = useState();
11702
- const [loading, setLoading] = useState(false);
11703
- const handleAuthSuccess = (token, user, accountData) => {
11704
- // Authentication successful
11705
- auth.login(token, user, accountData);
11706
- // If no additional questions, proceed directly to claim
11707
- if (additionalFields.length === 0) {
11708
- executeClaim(user);
11709
- }
11710
- else {
11711
- setClaimStep('questions');
11945
+ const handlePasswordReset = async (emailOrPassword, confirmPassword) => {
11946
+ setLoading(true);
11947
+ setError(undefined);
11948
+ try {
11949
+ if (resetToken && confirmPassword) {
11950
+ // Complete password reset with token
11951
+ await api.completePasswordReset(resetToken, emailOrPassword);
11952
+ setResetSuccess(true);
11953
+ setResetToken(undefined); // Clear token after successful reset
11954
+ }
11955
+ else {
11956
+ // Request password reset email
11957
+ await api.requestPasswordReset(emailOrPassword, getRedirectUrl());
11958
+ setResetSuccess(true);
11959
+ }
11712
11960
  }
11713
- };
11714
- const handleQuestionSubmit = async (e) => {
11715
- e.preventDefault();
11716
- // Validate required fields
11717
- const missingFields = additionalFields
11718
- .filter(field => field.required && !claimData[field.name])
11719
- .map(field => field.label);
11720
- if (missingFields.length > 0) {
11721
- setError(`Please fill in: ${missingFields.join(', ')}`);
11722
- return;
11961
+ catch (err) {
11962
+ const errorMessage = err instanceof Error ? err.message : 'Password reset failed';
11963
+ setError(errorMessage);
11964
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11723
11965
  }
11724
- // Execute claim with collected data
11725
- if (auth.user) {
11726
- executeClaim(auth.user);
11966
+ finally {
11967
+ setLoading(false);
11727
11968
  }
11728
11969
  };
11729
- const executeClaim = async (user) => {
11730
- setClaimStep('claiming');
11970
+ const handleMagicLink = async (email) => {
11731
11971
  setLoading(true);
11732
11972
  setError(undefined);
11733
11973
  try {
11734
- // Create attestation to claim the proof
11735
- const response = await smartlinks.attestation.create(collectionId, productId, proofId, {
11736
- public: {
11737
- claimed: true,
11738
- claimedAt: new Date().toISOString(),
11739
- claimedBy: user.uid,
11740
- ...claimData,
11741
- },
11742
- private: {},
11743
- proof: {},
11744
- });
11745
- setClaimStep('success');
11746
- // Call success callback
11747
- onClaimSuccess({
11748
- proofId,
11749
- user,
11750
- claimData,
11751
- attestationId: response.id,
11752
- });
11974
+ await api.sendMagicLink(email, getRedirectUrl());
11975
+ setAuthSuccess(true);
11976
+ setSuccessMessage('Magic link sent! Check your email to log in.');
11753
11977
  }
11754
11978
  catch (err) {
11755
- console.error('Claim error:', err);
11756
- const errorMessage = err instanceof Error ? err.message : 'Failed to claim proof';
11979
+ const errorMessage = err instanceof Error ? err.message : 'Failed to send magic link';
11757
11980
  setError(errorMessage);
11758
- onClaimError?.(err instanceof Error ? err : new Error(errorMessage));
11759
- setClaimStep(additionalFields.length > 0 ? 'questions' : 'auth');
11981
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11760
11982
  }
11761
11983
  finally {
11762
11984
  setLoading(false);
11763
11985
  }
11764
11986
  };
11765
- const handleFieldChange = (fieldName, value) => {
11766
- setClaimData(prev => ({
11767
- ...prev,
11768
- [fieldName]: value,
11769
- }));
11770
- };
11771
- // Render authentication step
11772
- if (claimStep === 'auth') {
11773
- return (jsx("div", { className: className, children: jsx(SmartlinksAuthUI, { apiEndpoint: apiEndpoint, clientId: clientId, clientName: clientName, onAuthSuccess: handleAuthSuccess, onAuthError: onClaimError, theme: theme, minimal: minimal, customization: customization.authConfig }) }));
11774
- }
11775
- // Render additional questions step
11776
- if (claimStep === 'questions') {
11777
- return (jsxs("div", { className: `claim-questions ${className}`, children: [jsxs("div", { className: "claim-header mb-6", children: [jsx("h2", { className: "text-2xl font-bold mb-2", children: customization.claimTitle || 'Complete Your Claim' }), customization.claimDescription && (jsx("p", { className: "text-muted-foreground", children: customization.claimDescription }))] }), error && (jsx("div", { className: "claim-error bg-destructive/10 text-destructive px-4 py-3 rounded-md mb-4", children: error })), jsxs("form", { onSubmit: handleQuestionSubmit, className: "claim-form space-y-4", children: [additionalFields.map((field) => (jsxs("div", { className: "claim-field", children: [jsxs("label", { htmlFor: field.name, className: "block text-sm font-medium mb-2", children: [field.label, field.required && jsx("span", { className: "text-destructive ml-1", children: "*" })] }), field.type === 'textarea' ? (jsx("textarea", { id: field.name, name: field.name, placeholder: field.placeholder, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background", rows: 4 })) : field.type === 'select' ? (jsxs("select", { id: field.name, name: field.name, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background", children: [jsx("option", { value: "", children: "Select..." }), field.options?.map((option) => (jsx("option", { value: option, children: option }, option)))] })) : (jsx("input", { id: field.name, name: field.name, type: field.type, placeholder: field.placeholder, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background" }))] }, field.name))), jsx("button", { type: "submit", disabled: loading, className: "claim-submit-button w-full bg-primary text-primary-foreground px-4 py-2 rounded-md font-medium hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed", children: loading ? 'Claiming...' : 'Submit Claim' })] })] }));
11987
+ if (configLoading) {
11988
+ return (jsx(AuthContainer, { theme: theme, className: className, minimal: minimal || config?.branding?.minimal || false, children: jsx("div", { style: { textAlign: 'center', padding: '2rem' }, children: jsx("div", { className: "auth-spinner" }) }) }));
11778
11989
  }
11779
- // Render claiming step (loading state)
11780
- if (claimStep === 'claiming') {
11781
- return (jsxs("div", { className: `claim-loading ${className} flex flex-col items-center justify-center py-12`, children: [jsx("div", { className: "claim-spinner w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin mb-4" }), jsx("p", { className: "text-muted-foreground", children: "Claiming your product..." })] }));
11782
- }
11783
- // Render success step
11784
- if (claimStep === 'success') {
11785
- return (jsxs("div", { className: `claim-success ${className} text-center py-12`, children: [jsx("div", { className: "claim-success-icon w-16 h-16 bg-green-500 text-white rounded-full flex items-center justify-center text-3xl font-bold mx-auto mb-4", children: "\u2713" }), jsx("h2", { className: "text-2xl font-bold mb-2", children: "Claim Successful!" }), jsx("p", { className: "text-muted-foreground", children: customization.successMessage || 'Your product has been successfully claimed and registered to your account.' })] }));
11786
- }
11787
- return null;
11788
- };
11789
-
11790
- const ProtectedRoute = ({ children, fallback, redirectTo, }) => {
11791
- const { isAuthenticated, isLoading } = useAuth();
11792
- // Show loading state
11793
- if (isLoading) {
11794
- return jsx("div", { children: "Loading..." });
11795
- }
11796
- // If not authenticated, redirect or show fallback
11797
- if (!isAuthenticated) {
11798
- if (redirectTo) {
11799
- window.location.href = redirectTo;
11800
- return null;
11801
- }
11802
- return fallback ? jsx(Fragment, { children: fallback }) : jsx("div", { children: "Access denied. Please log in." });
11803
- }
11804
- // Render protected content
11805
- return jsx(Fragment, { children: children });
11806
- };
11807
-
11808
- const AuthUIPreview = ({ customization, enabledProviders = ['email', 'google', 'phone'], providerOrder, emailDisplayMode = 'form', theme = 'light', className, minimal = false, }) => {
11809
- const showEmail = enabledProviders.includes('email');
11810
- const showGoogle = enabledProviders.includes('google');
11811
- const showPhone = enabledProviders.includes('phone');
11812
- const showMagicLink = enabledProviders.includes('magic-link');
11813
- // Determine ordered providers (excluding email if in button mode)
11814
- const orderedProviders = providerOrder && providerOrder.length > 0
11815
- ? providerOrder.filter(p => enabledProviders.includes(p) && p !== 'email')
11816
- : enabledProviders.filter(p => p !== 'email');
11817
- const hasOtherProviders = showGoogle || showPhone || showMagicLink;
11818
- // Render provider button helper
11819
- const renderProviderButton = (provider) => {
11820
- if (provider === 'google' && showGoogle) {
11821
- return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsxs("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "none", children: [jsx("path", { d: "M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z", fill: "#4285F4" }), jsx("path", { d: "M9 18c2.43 0 4.467-.806 5.956-2.183l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332C2.438 15.983 5.482 18 9 18z", fill: "#34A853" }), jsx("path", { d: "M3.964 10.707c-.18-.54-.282-1.117-.282-1.707 0-.593.102-1.167.282-1.707V4.961H.957C.347 6.175 0 7.548 0 9s.348 2.825.957 4.039l3.007-2.332z", fill: "#FBBC05" }), jsx("path", { d: "M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0 5.482 0 2.438 2.017.957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z", fill: "#EA4335" })] }), jsx("span", { children: "Continue with Google" })] }, "google"));
11822
- }
11823
- if (provider === 'phone' && showPhone) {
11824
- return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsx("path", { d: "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" }) }), jsx("span", { children: "Continue with Phone" })] }, "phone"));
11825
- }
11826
- if (provider === 'magic-link' && showMagicLink) {
11827
- return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }), jsx("span", { children: "Continue with Magic Link" })] }, "magic-link"));
11828
- }
11829
- if (provider === 'email' && showEmail && emailDisplayMode === 'button') {
11830
- return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }), jsx("span", { children: "Continue with Email" })] }, "email"));
11831
- }
11832
- return null;
11833
- };
11834
- return (jsx(AuthContainer, { theme: theme, className: className, config: customization, minimal: minimal, children: emailDisplayMode === 'button' ? (jsx("div", { className: "auth-provider-buttons", children: orderedProviders.concat(showEmail ? ['email'] : []).map(provider => renderProviderButton(provider)) })) : (
11835
- /* Form mode: show email form first, then other providers */
11836
- jsxs(Fragment, { children: [showEmail && (jsxs("div", { className: "auth-form", children: [jsxs("div", { className: "auth-form-group", children: [jsx("label", { className: "auth-label", children: "Email" }), jsx("input", { type: "email", className: "auth-input", placeholder: "Enter your email", disabled: true })] }), jsxs("div", { className: "auth-form-group", children: [jsx("label", { className: "auth-label", children: "Password" }), jsx("input", { type: "password", className: "auth-input", placeholder: "Enter your password", disabled: true })] }), jsx("button", { className: "auth-button auth-button-primary", disabled: true, children: "Sign In" }), jsx("div", { style: { textAlign: 'center', marginTop: '1rem' }, children: jsx("button", { className: "auth-link", disabled: true, children: "Forgot password?" }) }), jsxs("div", { style: { textAlign: 'center', marginTop: '0.5rem', fontSize: '0.875rem', color: 'var(--auth-text-muted, #6B7280)' }, children: ["Don't have an account?", ' ', jsx("button", { className: "auth-link", disabled: true, children: "Sign up" })] })] })), hasOtherProviders && (jsxs(Fragment, { children: [showEmail && (jsx("div", { className: "auth-or-divider", children: jsx("span", { children: "or continue with" }) })), jsx("div", { className: "auth-provider-buttons", children: orderedProviders.map(provider => renderProviderButton(provider)) })] }))] })) }));
11990
+ return (jsx(AuthContainer, { theme: theme, className: className, config: config, minimal: minimal || config?.branding?.minimal || false, children: authSuccess ? (jsxs("div", { style: { textAlign: 'center', padding: '2rem' }, children: [jsx("div", { style: {
11991
+ color: 'var(--auth-primary-color, #4F46E5)',
11992
+ fontSize: '3rem',
11993
+ marginBottom: '1rem'
11994
+ }, children: "\u2713" }), jsx("h2", { style: {
11995
+ marginBottom: '0.5rem',
11996
+ fontSize: '1.5rem',
11997
+ fontWeight: 600
11998
+ }, children: successMessage?.includes('verified') ? 'Email Verified!' :
11999
+ successMessage?.includes('Magic link') ? 'Check Your Email!' :
12000
+ mode === 'register' ? 'Account Created!' : 'Login Successful!' }), jsx("p", { style: {
12001
+ color: '#6B7280',
12002
+ fontSize: '0.875rem'
12003
+ }, children: successMessage })] })) : mode === 'magic-link' ? (jsx(MagicLinkForm, { onSubmit: handleMagicLink, onCancel: () => setMode('login'), loading: loading, error: error })) : mode === 'phone' ? (jsx(PhoneAuthForm, { onSubmit: handlePhoneAuth, onBack: () => setMode('login'), loading: loading, error: error })) : mode === 'reset-password' ? (jsx(PasswordResetForm, { onSubmit: handlePasswordReset, onBack: () => {
12004
+ setMode('login');
12005
+ setResetSuccess(false);
12006
+ setResetToken(undefined); // Clear token when going back
12007
+ }, loading: loading, error: error, success: resetSuccess, token: resetToken })) : (mode === 'login' || mode === 'register') ? (jsx(Fragment, { children: showResendVerification ? (jsxs("div", { style: { marginTop: '1rem', padding: '1.5rem', backgroundColor: 'rgba(79, 70, 229, 0.05)', borderRadius: '0.5rem' }, children: [jsx("h3", { style: { marginBottom: '0.75rem', fontSize: '1rem', fontWeight: 600, color: 'var(--auth-text-color, #374151)' }, children: "Verification Link Expired" }), jsx("p", { style: { marginBottom: '1rem', fontSize: '0.875rem', color: 'var(--auth-text-color, #6B7280)', lineHeight: '1.5' }, children: "Your verification link has expired or is no longer valid. Please enter your email address below and we'll send you a new verification link." }), jsx("input", { type: "email", value: resendEmail || '', onChange: (e) => setResendEmail(e.target.value), placeholder: "your@email.com", style: {
12008
+ width: '100%',
12009
+ padding: '0.625rem',
12010
+ marginBottom: '1rem',
12011
+ border: '1px solid var(--auth-border-color, #D1D5DB)',
12012
+ borderRadius: '0.375rem',
12013
+ fontSize: '0.875rem',
12014
+ boxSizing: 'border-box'
12015
+ } }), jsxs("div", { style: { display: 'flex', gap: '0.75rem' }, children: [jsx("button", { onClick: handleResendVerification, disabled: loading || !resendEmail, style: {
12016
+ flex: 1,
12017
+ padding: '0.625rem 1rem',
12018
+ backgroundColor: 'var(--auth-primary-color, #4F46E5)',
12019
+ color: 'white',
12020
+ border: 'none',
12021
+ borderRadius: '0.375rem',
12022
+ cursor: (loading || !resendEmail) ? 'not-allowed' : 'pointer',
12023
+ fontSize: '0.875rem',
12024
+ fontWeight: 500,
12025
+ opacity: (loading || !resendEmail) ? 0.6 : 1
12026
+ }, children: loading ? 'Sending...' : 'Send New Verification Link' }), jsx("button", { onClick: () => {
12027
+ setShowResendVerification(false);
12028
+ setResendEmail('');
12029
+ setError(undefined);
12030
+ }, disabled: loading, style: {
12031
+ padding: '0.625rem 1rem',
12032
+ backgroundColor: 'transparent',
12033
+ color: 'var(--auth-text-color, #6B7280)',
12034
+ border: '1px solid var(--auth-border-color, #D1D5DB)',
12035
+ borderRadius: '0.375rem',
12036
+ cursor: loading ? 'not-allowed' : 'pointer',
12037
+ fontSize: '0.875rem',
12038
+ fontWeight: 500,
12039
+ opacity: loading ? 0.6 : 1
12040
+ }, children: "Cancel" })] })] })) : showRequestNewReset ? (jsxs("div", { style: { marginTop: '1rem', padding: '1.5rem', backgroundColor: 'rgba(239, 68, 68, 0.05)', borderRadius: '0.5rem' }, children: [jsx("h3", { style: { marginBottom: '0.75rem', fontSize: '1rem', fontWeight: 600, color: 'var(--auth-text-color, #374151)' }, children: "Password Reset Link Expired" }), jsx("p", { style: { marginBottom: '1rem', fontSize: '0.875rem', color: 'var(--auth-text-color, #6B7280)', lineHeight: '1.5' }, children: "Your password reset link has expired or is no longer valid. Please enter your email address below and we'll send you a new password reset link." }), jsx("input", { type: "email", value: resetRequestEmail || '', onChange: (e) => setResetRequestEmail(e.target.value), placeholder: "your@email.com", style: {
12041
+ width: '100%',
12042
+ padding: '0.625rem',
12043
+ marginBottom: '1rem',
12044
+ border: '1px solid var(--auth-border-color, #D1D5DB)',
12045
+ borderRadius: '0.375rem',
12046
+ fontSize: '0.875rem',
12047
+ boxSizing: 'border-box'
12048
+ } }), jsxs("div", { style: { display: 'flex', gap: '0.75rem' }, children: [jsx("button", { onClick: handleRequestNewReset, disabled: loading || !resetRequestEmail, style: {
12049
+ flex: 1,
12050
+ padding: '0.625rem 1rem',
12051
+ backgroundColor: '#EF4444',
12052
+ color: 'white',
12053
+ border: 'none',
12054
+ borderRadius: '0.375rem',
12055
+ cursor: (loading || !resetRequestEmail) ? 'not-allowed' : 'pointer',
12056
+ fontSize: '0.875rem',
12057
+ fontWeight: 500,
12058
+ opacity: (loading || !resetRequestEmail) ? 0.6 : 1
12059
+ }, children: loading ? 'Sending...' : 'Send New Reset Link' }), jsx("button", { onClick: () => {
12060
+ setShowRequestNewReset(false);
12061
+ setResetRequestEmail('');
12062
+ setError(undefined);
12063
+ }, disabled: loading, style: {
12064
+ padding: '0.625rem 1rem',
12065
+ backgroundColor: 'transparent',
12066
+ color: 'var(--auth-text-color, #6B7280)',
12067
+ border: '1px solid var(--auth-border-color, #D1D5DB)',
12068
+ borderRadius: '0.375rem',
12069
+ cursor: loading ? 'not-allowed' : 'pointer',
12070
+ fontSize: '0.875rem',
12071
+ fontWeight: 500,
12072
+ opacity: loading ? 0.6 : 1
12073
+ }, children: "Cancel" })] })] })) : (jsx(Fragment, { children: (() => {
12074
+ const emailDisplayMode = config?.emailDisplayMode || 'form';
12075
+ const providerOrder = config?.providerOrder || (config?.enabledProviders || enabledProviders);
12076
+ const actualProviders = config?.enabledProviders || enabledProviders;
12077
+ // Button mode: show provider selection first, then email form if email is selected
12078
+ if (emailDisplayMode === 'button' && !showEmailForm) {
12079
+ return (jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onEmailLogin: () => setShowEmailForm(true), onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }));
12080
+ }
12081
+ // Form mode or email button was clicked: show email form with other providers
12082
+ return (jsxs(Fragment, { children: [emailDisplayMode === 'button' && showEmailForm && (jsx("button", { onClick: () => setShowEmailForm(false), style: {
12083
+ marginBottom: '1rem',
12084
+ padding: '0.5rem',
12085
+ background: 'none',
12086
+ border: 'none',
12087
+ color: 'var(--auth-text-color, #6B7280)',
12088
+ cursor: 'pointer',
12089
+ fontSize: '0.875rem',
12090
+ display: 'flex',
12091
+ alignItems: 'center',
12092
+ gap: '0.25rem'
12093
+ }, children: "\u2190 Back to options" })), jsx(EmailAuthForm, { mode: mode, onSubmit: handleEmailAuth, onModeSwitch: () => {
12094
+ setMode(mode === 'login' ? 'register' : 'login');
12095
+ setShowResendVerification(false);
12096
+ setShowRequestNewReset(false);
12097
+ setError(undefined);
12098
+ }, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error, additionalFields: config?.signupAdditionalFields }), emailDisplayMode === 'form' && actualProviders.length > 1 && (jsx(ProviderButtons, { enabledProviders: actualProviders.filter((p) => p !== 'email'), providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }))] }));
12099
+ })() })) })) : null }));
11837
12100
  };
11838
12101
 
11839
- // Default Smartlinks Google OAuth Client ID (public - safe to expose)
11840
- const DEFAULT_GOOGLE_CLIENT_ID = '696509063554-jdlbjl8vsjt7cr0vgkjkjf3ffnvi3a70.apps.googleusercontent.com';
11841
- const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, enabledProviders = ['email', 'google', 'phone'], initialMode = 'login', redirectUrl, theme = 'light', className, customization, skipConfigFetch = false, minimal = false, }) => {
11842
- const [mode, setMode] = useState(initialMode);
12102
+ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', customization = {}, }) => {
12103
+ const auth = useAuth();
11843
12104
  const [loading, setLoading] = useState(false);
12105
+ const [profile, setProfile] = useState(null);
11844
12106
  const [error, setError] = useState();
11845
- const [resetSuccess, setResetSuccess] = useState(false);
11846
- const [authSuccess, setAuthSuccess] = useState(false);
11847
- const [successMessage, setSuccessMessage] = useState();
11848
- const [showResendVerification, setShowResendVerification] = useState(false);
11849
- const [resendEmail, setResendEmail] = useState();
11850
- const [showRequestNewReset, setShowRequestNewReset] = useState(false);
11851
- const [resetRequestEmail, setResetRequestEmail] = useState();
11852
- const [resetToken, setResetToken] = useState(); // Store the reset token from URL
11853
- const [config, setConfig] = useState(null);
11854
- const [configLoading, setConfigLoading] = useState(!skipConfigFetch);
11855
- const [showEmailForm, setShowEmailForm] = useState(false); // Track if email form should be shown when emailDisplayMode is 'button'
11856
- const api = new AuthAPI(apiEndpoint, clientId, clientName);
11857
- const auth = useAuth();
11858
- // Reinitialize Smartlinks SDK when apiEndpoint changes (for test/dev scenarios)
11859
- // IMPORTANT: Preserve bearer token during reinitialization
11860
- useEffect(() => {
11861
- const reinitializeWithToken = async () => {
11862
- if (apiEndpoint) {
11863
- // Get current token before reinitializing
11864
- const currentToken = await auth.getToken();
11865
- smartlinks.initializeApi({
11866
- baseURL: apiEndpoint,
11867
- proxyMode: false, // Direct API calls when custom endpoint is provided
11868
- ngrokSkipBrowserWarning: true,
11869
- });
11870
- // Restore bearer token after reinitialization using auth.verifyToken
11871
- if (currentToken) {
11872
- smartlinks.auth.verifyToken(currentToken).catch(err => {
11873
- console.warn('Failed to restore bearer token after reinit:', err);
11874
- });
11875
- }
11876
- }
11877
- };
11878
- reinitializeWithToken();
11879
- }, [apiEndpoint, auth]);
11880
- // Get the effective redirect URL (use prop or default to current page)
11881
- const getRedirectUrl = () => {
11882
- if (redirectUrl)
11883
- return redirectUrl;
11884
- // Get the full current URL including hash routes
11885
- // Remove any existing query parameters to avoid duplication
11886
- const currentUrl = window.location.href.split('?')[0];
11887
- return currentUrl;
11888
- };
11889
- // Fetch UI configuration
11890
- useEffect(() => {
11891
- if (skipConfigFetch) {
11892
- setConfig(customization || {});
11893
- return;
11894
- }
11895
- const fetchConfig = async () => {
11896
- try {
11897
- // Check localStorage cache first
11898
- const cacheKey = `auth_ui_config_${clientId || 'default'}`;
11899
- const cached = localStorage.getItem(cacheKey);
11900
- if (cached) {
11901
- const { config: cachedConfig, timestamp } = JSON.parse(cached);
11902
- const age = Date.now() - timestamp;
11903
- // Use cache if less than 1 hour old
11904
- if (age < 3600000) {
11905
- setConfig({ ...cachedConfig, ...customization });
11906
- setConfigLoading(false);
11907
- // Fetch in background to update cache
11908
- api.fetchConfig().then(freshConfig => {
11909
- localStorage.setItem(cacheKey, JSON.stringify({
11910
- config: freshConfig,
11911
- timestamp: Date.now()
11912
- }));
11913
- });
11914
- return;
11915
- }
11916
- }
11917
- // Fetch from API
11918
- const fetchedConfig = await api.fetchConfig();
11919
- // Merge with customization props (props take precedence)
11920
- const mergedConfig = { ...fetchedConfig, ...customization };
11921
- setConfig(mergedConfig);
11922
- // Cache the fetched config
11923
- localStorage.setItem(cacheKey, JSON.stringify({
11924
- config: fetchedConfig,
11925
- timestamp: Date.now()
11926
- }));
11927
- }
11928
- catch (err) {
11929
- console.error('Failed to fetch config:', err);
11930
- setConfig(customization || {});
11931
- }
11932
- finally {
11933
- setConfigLoading(false);
11934
- }
11935
- };
11936
- fetchConfig();
11937
- }, [apiEndpoint, clientId, customization, skipConfigFetch]);
11938
- // Reset showEmailForm when mode changes away from login/register
12107
+ const [success, setSuccess] = useState();
12108
+ // Track which section is being edited
12109
+ const [editingSection, setEditingSection] = useState(null);
12110
+ // Profile form state
12111
+ const [displayName, setDisplayName] = useState('');
12112
+ // Email change state
12113
+ const [newEmail, setNewEmail] = useState('');
12114
+ const [emailPassword, setEmailPassword] = useState('');
12115
+ // Password change state
12116
+ const [currentPassword, setCurrentPassword] = useState('');
12117
+ const [newPassword, setNewPassword] = useState('');
12118
+ const [confirmPassword, setConfirmPassword] = useState('');
12119
+ // Phone change state (reuses existing sendPhoneCode flow)
12120
+ const [newPhone, setNewPhone] = useState('');
12121
+ const [phoneCode, setPhoneCode] = useState('');
12122
+ const [phoneCodeSent, setPhoneCodeSent] = useState(false);
12123
+ // Account deletion state
12124
+ const [deletePassword, setDeletePassword] = useState('');
12125
+ const [deleteConfirmText, setDeleteConfirmText] = useState('');
12126
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
12127
+ const { showProfileSection = true, showEmailSection = true, showPasswordSection = true, showPhoneSection = true, showDeleteAccount = false, } = customization;
12128
+ // Reinitialize Smartlinks SDK when apiEndpoint changes
11939
12129
  useEffect(() => {
11940
- if (mode !== 'login' && mode !== 'register') {
11941
- setShowEmailForm(false);
12130
+ if (apiEndpoint) {
12131
+ smartlinks.initializeApi({
12132
+ baseURL: apiEndpoint,
12133
+ proxyMode: false,
12134
+ ngrokSkipBrowserWarning: true,
12135
+ });
11942
12136
  }
11943
- }, [mode]);
11944
- // Handle URL-based auth flows (email verification, password reset)
12137
+ }, [apiEndpoint]);
12138
+ // Load user profile on mount
11945
12139
  useEffect(() => {
11946
- // Helper to get URL parameters from either hash or search
11947
- const getUrlParams = () => {
11948
- // First check if there are params in the hash (for hash routing)
11949
- const hash = window.location.hash;
11950
- const hashQueryIndex = hash.indexOf('?');
11951
- if (hashQueryIndex !== -1) {
11952
- // Extract query string from hash (e.g., #/test?mode=verifyEmail&token=abc)
11953
- const hashQuery = hash.substring(hashQueryIndex + 1);
11954
- return new URLSearchParams(hashQuery);
11955
- }
11956
- // Fall back to regular search params (for non-hash routing)
11957
- return new URLSearchParams(window.location.search);
11958
- };
11959
- const params = getUrlParams();
11960
- const urlMode = params.get('mode');
11961
- const token = params.get('token');
11962
- console.log('URL params detected:', { urlMode, token, hash: window.location.hash, search: window.location.search });
11963
- if (urlMode && token) {
11964
- handleURLBasedAuth(urlMode, token);
12140
+ loadProfile();
12141
+ }, [clientId]);
12142
+ const loadProfile = async () => {
12143
+ if (!auth.isAuthenticated) {
12144
+ setError('You must be logged in to manage your account');
12145
+ return;
11965
12146
  }
11966
- }, []);
11967
- const handleURLBasedAuth = async (urlMode, token) => {
11968
12147
  setLoading(true);
11969
12148
  setError(undefined);
11970
12149
  try {
11971
- if (urlMode === 'verifyEmail') {
11972
- console.log('Verifying email with token:', token);
11973
- const response = await api.verifyEmailWithToken(token);
11974
- // Get email verification mode from response or config
11975
- const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
11976
- if ((verificationMode === 'verify-then-auto-login' || verificationMode === 'immediate') && response.token) {
11977
- // Auto-login modes: Log the user in immediately if token is provided
11978
- auth.login(response.token, response.user, response.accountData);
11979
- setAuthSuccess(true);
11980
- setSuccessMessage('Email verified successfully! You are now logged in.');
11981
- onAuthSuccess(response.token, response.user, response.accountData);
11982
- // Clear the URL parameters
11983
- const cleanUrl = window.location.href.split('?')[0];
11984
- window.history.replaceState({}, document.title, cleanUrl);
11985
- // Redirect after a brief delay to show success message
11986
- if (redirectUrl) {
11987
- setTimeout(() => {
11988
- window.location.href = redirectUrl;
11989
- }, 2000);
11990
- }
11991
- }
11992
- else {
11993
- // verify-then-manual-login mode or no token: Show success but require manual login
11994
- setAuthSuccess(true);
11995
- setSuccessMessage('Email verified successfully! Please log in with your credentials.');
11996
- // Clear the URL parameters
11997
- const cleanUrl = window.location.href.split('?')[0];
11998
- window.history.replaceState({}, document.title, cleanUrl);
11999
- // Switch back to login mode after a delay
12000
- setTimeout(() => {
12001
- setAuthSuccess(false);
12002
- setMode('login');
12003
- }, 3000);
12004
- }
12005
- }
12006
- else if (urlMode === 'resetPassword') {
12007
- console.log('Verifying reset token:', token);
12008
- // Verify token is valid, then show password reset form
12009
- await api.verifyResetToken(token);
12010
- setResetToken(token); // Store token for use in password reset
12011
- setMode('reset-password');
12012
- // Clear the URL parameters
12013
- const cleanUrl = window.location.href.split('?')[0];
12014
- window.history.replaceState({}, document.title, cleanUrl);
12015
- }
12016
- else if (urlMode === 'magicLink') {
12017
- console.log('Verifying magic link token:', token);
12018
- const response = await api.verifyMagicLink(token);
12019
- // Auto-login with magic link if token is provided
12020
- if (response.token) {
12021
- auth.login(response.token, response.user, response.accountData);
12022
- setAuthSuccess(true);
12023
- setSuccessMessage('Magic link verified! You are now logged in.');
12024
- onAuthSuccess(response.token, response.user, response.accountData);
12025
- // Clear the URL parameters
12026
- const cleanUrl = window.location.href.split('?')[0];
12027
- window.history.replaceState({}, document.title, cleanUrl);
12028
- // Redirect after a brief delay to show success message
12029
- if (redirectUrl) {
12030
- setTimeout(() => {
12031
- window.location.href = redirectUrl;
12032
- }, 2000);
12033
- }
12034
- }
12035
- else {
12036
- throw new Error('Authentication failed - no token received');
12037
- }
12038
- }
12150
+ // TODO: Backend implementation required
12151
+ // Endpoint: GET /api/v1/authkit/:clientId/account/profile
12152
+ // SDK method: smartlinks.authKit.getProfile(clientId)
12153
+ // Temporary mock data for UI testing
12154
+ const profileData = {
12155
+ uid: auth.user?.uid || '',
12156
+ email: auth.user?.email,
12157
+ displayName: auth.user?.displayName,
12158
+ phoneNumber: auth.user?.phoneNumber,
12159
+ photoURL: auth.user?.photoURL,
12160
+ emailVerified: true,
12161
+ accountData: auth.accountData || {},
12162
+ };
12163
+ setProfile(profileData);
12164
+ setDisplayName(profileData.displayName || '');
12039
12165
  }
12040
12166
  catch (err) {
12041
- console.error('URL-based auth error:', err);
12042
- const errorMessage = err instanceof Error ? err.message : 'An error occurred';
12043
- // If it's an email verification error (expired/invalid token), show resend option
12044
- if (urlMode === 'verifyEmail') {
12045
- setError(`${errorMessage} - Please enter your email below to receive a new verification link.`);
12046
- setShowResendVerification(true);
12047
- setMode('login'); // Show the login form UI
12048
- // Clear the URL parameters
12049
- const cleanUrl = window.location.href.split('?')[0];
12050
- window.history.replaceState({}, document.title, cleanUrl);
12051
- }
12052
- else if (urlMode === 'resetPassword') {
12053
- // If password reset token is invalid/expired, show request new reset link option
12054
- setError(`${errorMessage} - Please enter your email below to receive a new password reset link.`);
12055
- setShowRequestNewReset(true);
12056
- setMode('login');
12057
- // Clear the URL parameters
12058
- const cleanUrl = window.location.href.split('?')[0];
12059
- window.history.replaceState({}, document.title, cleanUrl);
12060
- }
12061
- else if (urlMode === 'magicLink') {
12062
- // If magic link is invalid/expired
12063
- setError(`${errorMessage} - Please request a new magic link below.`);
12064
- setMode('magic-link');
12065
- // Clear the URL parameters
12066
- const cleanUrl = window.location.href.split('?')[0];
12067
- window.history.replaceState({}, document.title, cleanUrl);
12068
- }
12069
- else {
12070
- setError(errorMessage);
12071
- }
12072
- onAuthError?.(err instanceof Error ? err : new Error('An error occurred'));
12167
+ const errorMessage = err instanceof Error ? err.message : 'Failed to load profile';
12168
+ setError(errorMessage);
12169
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
12073
12170
  }
12074
12171
  finally {
12075
12172
  setLoading(false);
12076
12173
  }
12077
12174
  };
12078
- const handleEmailAuth = async (data) => {
12175
+ const handleUpdateProfile = async (e) => {
12176
+ e.preventDefault();
12079
12177
  setLoading(true);
12080
12178
  setError(undefined);
12081
- setAuthSuccess(false);
12179
+ setSuccess(undefined);
12082
12180
  try {
12083
- const response = mode === 'login'
12084
- ? await api.login(data.email, data.password)
12085
- : await api.register({
12086
- ...data,
12087
- accountData: mode === 'register' ? accountData : undefined,
12088
- redirectUrl: getRedirectUrl(), // Include redirect URL for email verification
12089
- });
12090
- // Get email verification mode from response or config (default: verify-then-auto-login)
12091
- const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
12092
- const gracePeriodHours = config?.emailVerification?.gracePeriodHours || 24;
12093
- if (mode === 'register') {
12094
- // Handle different verification modes
12095
- if (verificationMode === 'immediate' && response.token) {
12096
- // Immediate mode: Log in right away if token is provided
12097
- auth.login(response.token, response.user, response.accountData);
12098
- setAuthSuccess(true);
12099
- const deadline = response.emailVerificationDeadline
12100
- ? new Date(response.emailVerificationDeadline).toLocaleString()
12101
- : `${gracePeriodHours} hours`;
12102
- setSuccessMessage(`Account created! You're logged in now. Please verify your email by ${deadline} to keep your account active.`);
12103
- if (response.token) {
12104
- onAuthSuccess(response.token, response.user, response.accountData);
12105
- }
12106
- if (redirectUrl) {
12107
- setTimeout(() => {
12108
- window.location.href = redirectUrl;
12109
- }, 2000);
12110
- }
12111
- }
12112
- else if (verificationMode === 'verify-then-auto-login') {
12113
- // Verify-then-auto-login mode: Don't log in yet, but will auto-login after email verification
12114
- setAuthSuccess(true);
12115
- setSuccessMessage('Account created! Please check your email and click the verification link to complete your registration.');
12116
- }
12117
- else {
12118
- // verify-then-manual-login mode: Traditional flow
12119
- setAuthSuccess(true);
12120
- setSuccessMessage('Account created successfully! Please check your email to verify your account, then log in.');
12121
- }
12122
- }
12123
- else {
12124
- // Login mode - always log in if token is provided
12125
- if (response.token) {
12126
- // Check for account lock or verification requirements
12127
- if (response.accountLocked) {
12128
- throw new Error('Your account has been locked due to unverified email. Please check your email or request a new verification link.');
12129
- }
12130
- if (response.requiresEmailVerification) {
12131
- throw new Error('Please verify your email before logging in. Check your inbox for the verification link.');
12132
- }
12133
- auth.login(response.token, response.user, response.accountData);
12134
- setAuthSuccess(true);
12135
- setSuccessMessage('Login successful!');
12136
- onAuthSuccess(response.token, response.user, response.accountData);
12137
- if (redirectUrl) {
12138
- setTimeout(() => {
12139
- window.location.href = redirectUrl;
12140
- }, 2000);
12141
- }
12142
- }
12143
- else {
12144
- throw new Error('Authentication failed - please verify your email before logging in.');
12145
- }
12146
- }
12181
+ // TODO: Backend implementation required
12182
+ // Endpoint: POST /api/v1/authkit/:clientId/account/update-profile
12183
+ // SDK method: smartlinks.authKit.updateProfile(clientId, updateData)
12184
+ setError('Backend API not yet implemented. See console for required endpoint.');
12185
+ console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-profile');
12186
+ console.log('Update data:', { displayName });
12187
+ // Uncomment when backend is ready:
12188
+ // const updateData: ProfileUpdateData = {
12189
+ // displayName: displayName || undefined,
12190
+ // photoURL: photoURL || undefined,
12191
+ // };
12192
+ // const updatedProfile = await smartlinks.authKit.updateProfile(clientId, updateData);
12193
+ // setProfile(updatedProfile);
12194
+ // setSuccess('Profile updated successfully!');
12195
+ // setEditingSection(null);
12196
+ // onProfileUpdated?.(updatedProfile);
12147
12197
  }
12148
12198
  catch (err) {
12149
- const errorMessage = err instanceof Error ? err.message : 'Authentication failed';
12150
- // Check if error is about email already registered
12151
- if (mode === 'register' && errorMessage.toLowerCase().includes('already') && errorMessage.toLowerCase().includes('email')) {
12152
- setShowResendVerification(true);
12153
- setResendEmail(data.email);
12154
- setError('This email is already registered. If you didn\'t receive the verification email, you can resend it below.');
12155
- }
12156
- else {
12157
- setError(errorMessage);
12158
- }
12159
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
12199
+ const errorMessage = err instanceof Error ? err.message : 'Failed to update profile';
12200
+ setError(errorMessage);
12201
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
12202
+ }
12203
+ finally {
12204
+ setLoading(false);
12205
+ }
12206
+ };
12207
+ const cancelEdit = () => {
12208
+ setEditingSection(null);
12209
+ setDisplayName(profile?.displayName || '');
12210
+ setNewEmail('');
12211
+ setEmailPassword('');
12212
+ setCurrentPassword('');
12213
+ setNewPassword('');
12214
+ setConfirmPassword('');
12215
+ setNewPhone('');
12216
+ setPhoneCode('');
12217
+ setPhoneCodeSent(false);
12218
+ setError(undefined);
12219
+ setSuccess(undefined);
12220
+ };
12221
+ const handleChangeEmail = async (e) => {
12222
+ e.preventDefault();
12223
+ setLoading(true);
12224
+ setError(undefined);
12225
+ setSuccess(undefined);
12226
+ try {
12227
+ // TODO: Backend implementation required
12228
+ // Endpoint: POST /api/v1/authkit/:clientId/account/change-email
12229
+ // SDK method: smartlinks.authKit.changeEmail(clientId, newEmail, password)
12230
+ // Note: No verification flow for now - direct email update
12231
+ setError('Backend API not yet implemented. See console for required endpoint.');
12232
+ console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-email');
12233
+ console.log('Data:', { newEmail });
12234
+ // Uncomment when backend is ready:
12235
+ // await smartlinks.authKit.changeEmail(clientId, newEmail, emailPassword);
12236
+ // setSuccess('Email changed successfully!');
12237
+ // setEditingSection(null);
12238
+ // setNewEmail('');
12239
+ // setEmailPassword('');
12240
+ // onEmailChangeRequested?.();
12241
+ // await loadProfile(); // Reload to show new email
12242
+ }
12243
+ catch (err) {
12244
+ const errorMessage = err instanceof Error ? err.message : 'Failed to change email';
12245
+ setError(errorMessage);
12246
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
12160
12247
  }
12161
12248
  finally {
12162
12249
  setLoading(false);
12163
12250
  }
12164
12251
  };
12165
- const handleResendVerification = async () => {
12166
- if (!resendEmail)
12252
+ const handleChangePassword = async (e) => {
12253
+ e.preventDefault();
12254
+ if (newPassword !== confirmPassword) {
12255
+ setError('New passwords do not match');
12256
+ return;
12257
+ }
12258
+ if (newPassword.length < 6) {
12259
+ setError('Password must be at least 6 characters');
12167
12260
  return;
12261
+ }
12168
12262
  setLoading(true);
12169
12263
  setError(undefined);
12264
+ setSuccess(undefined);
12170
12265
  try {
12171
- // For resend, we need the userId. If we don't have it, we need to handle this differently
12172
- // The backend should ideally handle this case
12173
- await api.resendVerification('unknown', resendEmail, getRedirectUrl());
12174
- setAuthSuccess(true);
12175
- setSuccessMessage('Verification email sent! Please check your inbox.');
12176
- setShowResendVerification(false);
12266
+ // TODO: Backend implementation required
12267
+ // Endpoint: POST /api/v1/authkit/:clientId/account/change-password
12268
+ // SDK method: smartlinks.authKit.changePassword(clientId, currentPassword, newPassword)
12269
+ setError('Backend API not yet implemented. See console for required endpoint.');
12270
+ console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-password');
12271
+ console.log('Data: currentPassword and newPassword provided');
12272
+ // Uncomment when backend is ready:
12273
+ // await smartlinks.authKit.changePassword(clientId, currentPassword, newPassword);
12274
+ // setSuccess('Password changed successfully!');
12275
+ // setEditingSection(null);
12276
+ // setCurrentPassword('');
12277
+ // setNewPassword('');
12278
+ // setConfirmPassword('');
12279
+ // onPasswordChanged?.();
12177
12280
  }
12178
12281
  catch (err) {
12179
- const errorMessage = err instanceof Error ? err.message : 'Failed to resend verification email';
12282
+ const errorMessage = err instanceof Error ? err.message : 'Failed to change password';
12180
12283
  setError(errorMessage);
12181
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
12284
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
12182
12285
  }
12183
12286
  finally {
12184
12287
  setLoading(false);
12185
12288
  }
12186
12289
  };
12187
- const handleRequestNewReset = async () => {
12188
- if (!resetRequestEmail)
12189
- return;
12290
+ const handleSendPhoneCode = async () => {
12190
12291
  setLoading(true);
12191
12292
  setError(undefined);
12192
12293
  try {
12193
- await api.requestPasswordReset(resetRequestEmail, getRedirectUrl());
12194
- setAuthSuccess(true);
12195
- setSuccessMessage('Password reset email sent! Please check your inbox.');
12196
- setShowRequestNewReset(false);
12197
- setResetRequestEmail('');
12294
+ await smartlinks.authKit.sendPhoneCode(clientId, newPhone);
12295
+ setPhoneCodeSent(true);
12296
+ setSuccess('Verification code sent to your phone');
12198
12297
  }
12199
12298
  catch (err) {
12200
- const errorMessage = err instanceof Error ? err.message : 'Failed to send password reset email';
12299
+ const errorMessage = err instanceof Error ? err.message : 'Failed to send verification code';
12201
12300
  setError(errorMessage);
12202
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
12301
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
12203
12302
  }
12204
12303
  finally {
12205
12304
  setLoading(false);
12206
12305
  }
12207
12306
  };
12208
- const handleGoogleLogin = async () => {
12209
- // Use custom client ID from config, or fall back to default Smartlinks client ID
12210
- const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
12211
- // Determine OAuth flow: default to 'oneTap' for better UX, but allow 'popup' for iframe compatibility
12212
- const oauthFlow = config?.googleOAuthFlow || 'oneTap';
12307
+ const handleUpdatePhone = async (e) => {
12308
+ e.preventDefault();
12213
12309
  setLoading(true);
12214
12310
  setError(undefined);
12311
+ setSuccess(undefined);
12215
12312
  try {
12216
- const google = window.google;
12217
- if (!google) {
12218
- throw new Error('Google Identity Services not loaded. Please check your internet connection.');
12219
- }
12220
- if (oauthFlow === 'popup') {
12221
- // Use OAuth2 popup flow (works in iframes but requires popup permission)
12222
- if (!google.accounts.oauth2) {
12223
- throw new Error('Google OAuth2 not available');
12224
- }
12225
- const client = google.accounts.oauth2.initTokenClient({
12226
- client_id: googleClientId,
12227
- scope: 'openid email profile',
12228
- callback: async (response) => {
12229
- try {
12230
- if (response.error) {
12231
- throw new Error(response.error_description || response.error);
12232
- }
12233
- const accessToken = response.access_token;
12234
- // Send access token to backend
12235
- const authResponse = await api.loginWithGoogle(accessToken);
12236
- if (authResponse.token) {
12237
- auth.login(authResponse.token, authResponse.user, authResponse.accountData);
12238
- setAuthSuccess(true);
12239
- setSuccessMessage('Google login successful!');
12240
- onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
12241
- }
12242
- else {
12243
- throw new Error('Authentication failed - no token received');
12244
- }
12245
- if (redirectUrl) {
12246
- setTimeout(() => {
12247
- window.location.href = redirectUrl;
12248
- }, 2000);
12249
- }
12250
- setLoading(false);
12251
- }
12252
- catch (err) {
12253
- const errorMessage = err instanceof Error ? err.message : 'Google login failed';
12254
- setError(errorMessage);
12255
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
12256
- setLoading(false);
12257
- }
12258
- },
12259
- });
12260
- client.requestAccessToken();
12261
- }
12262
- else {
12263
- // Use One Tap / Sign-In button flow (smoother UX but doesn't work in iframes)
12264
- google.accounts.id.initialize({
12265
- client_id: googleClientId,
12266
- callback: async (response) => {
12267
- try {
12268
- const idToken = response.credential;
12269
- const authResponse = await api.loginWithGoogle(idToken);
12270
- if (authResponse.token) {
12271
- auth.login(authResponse.token, authResponse.user, authResponse.accountData);
12272
- setAuthSuccess(true);
12273
- setSuccessMessage('Google login successful!');
12274
- onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
12275
- }
12276
- else {
12277
- throw new Error('Authentication failed - no token received');
12278
- }
12279
- if (redirectUrl) {
12280
- setTimeout(() => {
12281
- window.location.href = redirectUrl;
12282
- }, 2000);
12283
- }
12284
- setLoading(false);
12285
- }
12286
- catch (err) {
12287
- const errorMessage = err instanceof Error ? err.message : 'Google login failed';
12288
- setError(errorMessage);
12289
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
12290
- setLoading(false);
12291
- }
12292
- },
12293
- auto_select: false,
12294
- cancel_on_tap_outside: true,
12295
- });
12296
- google.accounts.id.prompt((notification) => {
12297
- if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
12298
- setLoading(false);
12299
- }
12300
- });
12301
- }
12313
+ // TODO: Backend implementation required
12314
+ // Endpoint: POST /api/v1/authkit/:clientId/account/update-phone
12315
+ // SDK method: smartlinks.authKit.updatePhone(clientId, phoneNumber, verificationCode)
12316
+ setError('Backend API not yet implemented. See console for required endpoint.');
12317
+ console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-phone');
12318
+ console.log('Data:', { phoneNumber: newPhone, code: phoneCode });
12319
+ // Uncomment when backend is ready:
12320
+ // await smartlinks.authKit.updatePhone(clientId, newPhone, phoneCode);
12321
+ // setSuccess('Phone number updated successfully!');
12322
+ // setEditingSection(null);
12323
+ // setNewPhone('');
12324
+ // setPhoneCode('');
12325
+ // setPhoneCodeSent(false);
12326
+ // await loadProfile();
12302
12327
  }
12303
12328
  catch (err) {
12304
- const errorMessage = err instanceof Error ? err.message : 'Google login failed';
12329
+ const errorMessage = err instanceof Error ? err.message : 'Failed to update phone number';
12305
12330
  setError(errorMessage);
12306
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
12331
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
12332
+ }
12333
+ finally {
12307
12334
  setLoading(false);
12308
12335
  }
12309
12336
  };
12310
- const handlePhoneAuth = async (phoneNumber, verificationCode) => {
12337
+ const handleDeleteAccount = async () => {
12338
+ if (deleteConfirmText !== 'DELETE') {
12339
+ setError('Please type DELETE to confirm account deletion');
12340
+ return;
12341
+ }
12342
+ if (!deletePassword) {
12343
+ setError('Password is required');
12344
+ return;
12345
+ }
12311
12346
  setLoading(true);
12312
12347
  setError(undefined);
12313
12348
  try {
12314
- if (!verificationCode) {
12315
- // Send verification code via Twilio Verify Service
12316
- await api.sendPhoneCode(phoneNumber);
12317
- // Twilio Verify Service tracks the verification by phone number
12318
- // No need to store verificationId
12319
- }
12320
- else {
12321
- // Verify code - Twilio identifies the verification by phone number
12322
- const response = await api.verifyPhoneCode(phoneNumber, verificationCode);
12323
- // Update auth context with account data if token is provided
12324
- if (response.token) {
12325
- auth.login(response.token, response.user, response.accountData);
12326
- onAuthSuccess(response.token, response.user, response.accountData);
12327
- if (redirectUrl) {
12328
- window.location.href = redirectUrl;
12329
- }
12330
- }
12331
- else {
12332
- throw new Error('Authentication failed - no token received');
12333
- }
12334
- }
12349
+ // TODO: Backend implementation required
12350
+ // Endpoint: DELETE /api/v1/authkit/:clientId/account/delete
12351
+ // SDK method: smartlinks.authKit.deleteAccount(clientId, password, confirmText)
12352
+ // Note: This performs a SOFT DELETE (marks as deleted, obfuscates email)
12353
+ setError('Backend API not yet implemented. See console for required endpoint.');
12354
+ console.log('Required API endpoint: DELETE /api/v1/authkit/:clientId/account/delete');
12355
+ console.log('Data: password and confirmText="DELETE" provided');
12356
+ console.log('Note: Backend should soft delete (mark deleted, obfuscate email, disable account)');
12357
+ // Uncomment when backend is ready:
12358
+ // await smartlinks.authKit.deleteAccount(clientId, deletePassword, deleteConfirmText);
12359
+ // setSuccess('Account deleted successfully');
12360
+ // onAccountDeleted?.();
12361
+ // await auth.logout();
12362
+ }
12363
+ catch (err) {
12364
+ const errorMessage = err instanceof Error ? err.message : 'Failed to delete account';
12365
+ setError(errorMessage);
12366
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
12367
+ }
12368
+ finally {
12369
+ setLoading(false);
12370
+ }
12371
+ };
12372
+ if (!auth.isAuthenticated) {
12373
+ return (jsx("div", { className: `account-management ${className}`, children: jsx("p", { className: "text-muted-foreground", children: "Please log in to manage your account" }) }));
12374
+ }
12375
+ if (loading && !profile) {
12376
+ return (jsx("div", { className: `account-management ${className}`, children: jsx("p", { className: "text-muted-foreground", children: "Loading..." }) }));
12377
+ }
12378
+ return (jsxs("div", { className: `account-management ${className}`, style: { maxWidth: '600px' }, children: [error && (jsx("div", { className: "auth-error", role: "alert", children: error })), success && (jsx("div", { className: "auth-success", role: "alert", children: success })), showProfileSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Display Name" }), jsx("div", { className: "field-value", children: profile?.displayName || 'Not set' })] }), editingSection !== 'profile' && (jsx("button", { type: "button", onClick: () => setEditingSection('profile'), className: "auth-button button-secondary", children: "Change" }))] }), editingSection === 'profile' && (jsxs("form", { onSubmit: handleUpdateProfile, className: "edit-form", children: [jsx("div", { className: "form-group", children: jsx("input", { id: "displayName", type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "Your display name", className: "auth-input" }) }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showEmailSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Email Address" }), jsxs("div", { className: "field-value", children: [profile?.email || 'Not set', profile?.emailVerified && (jsx("span", { className: "verification-badge verified", children: "Verified" })), profile?.email && !profile?.emailVerified && (jsx("span", { className: "verification-badge unverified", children: "Unverified" }))] })] }), editingSection !== 'email' && (jsx("button", { type: "button", onClick: () => setEditingSection('email'), className: "auth-button button-secondary", children: "Change Email" }))] }), editingSection === 'email' && (jsxs("form", { onSubmit: handleChangeEmail, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newEmail", children: "New Email" }), jsx("input", { id: "newEmail", type: "email", value: newEmail, onChange: (e) => setNewEmail(e.target.value), placeholder: "new.email@example.com", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "emailPassword", children: "Confirm Password" }), jsx("input", { id: "emailPassword", type: "password", value: emailPassword, onChange: (e) => setEmailPassword(e.target.value), placeholder: "Enter your password", className: "auth-input", required: true })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Email' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPasswordSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Password" }), jsx("div", { className: "field-value", children: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] }), editingSection !== 'password' && (jsx("button", { type: "button", onClick: () => setEditingSection('password'), className: "auth-button button-secondary", children: "Change Password" }))] }), editingSection === 'password' && (jsxs("form", { onSubmit: handleChangePassword, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "currentPassword", children: "Current Password" }), jsx("input", { id: "currentPassword", type: "password", value: currentPassword, onChange: (e) => setCurrentPassword(e.target.value), placeholder: "Enter current password", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPassword", children: "New Password" }), jsx("input", { id: "newPassword", type: "password", value: newPassword, onChange: (e) => setNewPassword(e.target.value), placeholder: "Enter new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "confirmPassword", children: "Confirm New Password" }), jsx("input", { id: "confirmPassword", type: "password", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), placeholder: "Confirm new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Password' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPhoneSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Phone Number" }), jsx("div", { className: "field-value", children: profile?.phoneNumber || 'Not set' })] }), editingSection !== 'phone' && (jsx("button", { type: "button", onClick: () => setEditingSection('phone'), className: "auth-button button-secondary", children: "Change Phone" }))] }), editingSection === 'phone' && (jsxs("form", { onSubmit: handleUpdatePhone, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPhone", children: "New Phone Number" }), jsx("input", { id: "newPhone", type: "tel", value: newPhone, onChange: (e) => setNewPhone(e.target.value), placeholder: "+1234567890", className: "auth-input", required: true })] }), !phoneCodeSent ? (jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleSendPhoneCode, className: "auth-button", disabled: loading || !newPhone, children: loading ? 'Sending...' : 'Send Code' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })) : (jsxs(Fragment, { children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "phoneCode", children: "Verification Code" }), jsx("input", { id: "phoneCode", type: "text", value: phoneCode, onChange: (e) => setPhoneCode(e.target.value), placeholder: "Enter 6-digit code", className: "auth-input", required: true, maxLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Verifying...' : 'Verify & Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] })), showDeleteAccount && (jsxs("section", { className: "account-section danger-zone", children: [jsx("h3", { className: "section-title text-danger", children: "Danger Zone" }), !showDeleteConfirm ? (jsx("button", { type: "button", onClick: () => setShowDeleteConfirm(true), className: "auth-button button-danger", children: "Delete Account" })) : (jsxs("div", { className: "delete-confirm", children: [jsx("p", { className: "warning-text", children: "\u26A0\uFE0F This action cannot be undone. This will permanently delete your account and all associated data." }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deletePassword", children: "Confirm Password" }), jsx("input", { id: "deletePassword", type: "password", value: deletePassword, onChange: (e) => setDeletePassword(e.target.value), placeholder: "Enter your password", className: "auth-input" })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deleteConfirm", children: "Type DELETE to confirm" }), jsx("input", { id: "deleteConfirm", type: "text", value: deleteConfirmText, onChange: (e) => setDeleteConfirmText(e.target.value), placeholder: "DELETE", className: "auth-input" })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleDeleteAccount, className: "auth-button button-danger", disabled: loading, children: loading ? 'Deleting...' : 'Permanently Delete Account' }), jsx("button", { type: "button", onClick: () => {
12379
+ setShowDeleteConfirm(false);
12380
+ setDeletePassword('');
12381
+ setDeleteConfirmText('');
12382
+ }, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] }));
12383
+ };
12384
+
12385
+ const SmartlinksClaimUI = ({ apiEndpoint, clientId, clientName, collectionId, productId, proofId, onClaimSuccess, onClaimError, additionalFields = [], theme = 'light', className = '', minimal = false, customization = {}, }) => {
12386
+ const auth = useAuth();
12387
+ const [claimStep, setClaimStep] = useState(auth.isAuthenticated ? 'questions' : 'auth');
12388
+ const [claimData, setClaimData] = useState({});
12389
+ const [error, setError] = useState();
12390
+ const [loading, setLoading] = useState(false);
12391
+ const handleAuthSuccess = (token, user, accountData) => {
12392
+ // Authentication successful
12393
+ auth.login(token, user, accountData);
12394
+ // If no additional questions, proceed directly to claim
12395
+ if (additionalFields.length === 0) {
12396
+ executeClaim(user);
12335
12397
  }
12336
- catch (err) {
12337
- const errorMessage = err instanceof Error ? err.message : 'Phone authentication failed';
12338
- setError(errorMessage);
12339
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
12398
+ else {
12399
+ setClaimStep('questions');
12340
12400
  }
12341
- finally {
12342
- setLoading(false);
12401
+ };
12402
+ const handleQuestionSubmit = async (e) => {
12403
+ e.preventDefault();
12404
+ // Validate required fields
12405
+ const missingFields = additionalFields
12406
+ .filter(field => field.required && !claimData[field.name])
12407
+ .map(field => field.label);
12408
+ if (missingFields.length > 0) {
12409
+ setError(`Please fill in: ${missingFields.join(', ')}`);
12410
+ return;
12411
+ }
12412
+ // Execute claim with collected data
12413
+ if (auth.user) {
12414
+ executeClaim(auth.user);
12343
12415
  }
12344
12416
  };
12345
- const handlePasswordReset = async (emailOrPassword, confirmPassword) => {
12417
+ const executeClaim = async (user) => {
12418
+ setClaimStep('claiming');
12346
12419
  setLoading(true);
12347
12420
  setError(undefined);
12348
12421
  try {
12349
- if (resetToken && confirmPassword) {
12350
- // Complete password reset with token
12351
- await api.completePasswordReset(resetToken, emailOrPassword);
12352
- setResetSuccess(true);
12353
- setResetToken(undefined); // Clear token after successful reset
12354
- }
12355
- else {
12356
- // Request password reset email
12357
- await api.requestPasswordReset(emailOrPassword, getRedirectUrl());
12358
- setResetSuccess(true);
12359
- }
12422
+ // Create attestation to claim the proof
12423
+ const response = await smartlinks.attestation.create(collectionId, productId, proofId, {
12424
+ public: {
12425
+ claimed: true,
12426
+ claimedAt: new Date().toISOString(),
12427
+ claimedBy: user.uid,
12428
+ ...claimData,
12429
+ },
12430
+ private: {},
12431
+ proof: {},
12432
+ });
12433
+ setClaimStep('success');
12434
+ // Call success callback
12435
+ onClaimSuccess({
12436
+ proofId,
12437
+ user,
12438
+ claimData,
12439
+ attestationId: response.id,
12440
+ });
12360
12441
  }
12361
12442
  catch (err) {
12362
- const errorMessage = err instanceof Error ? err.message : 'Password reset failed';
12443
+ console.error('Claim error:', err);
12444
+ const errorMessage = err instanceof Error ? err.message : 'Failed to claim proof';
12363
12445
  setError(errorMessage);
12364
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
12446
+ onClaimError?.(err instanceof Error ? err : new Error(errorMessage));
12447
+ setClaimStep(additionalFields.length > 0 ? 'questions' : 'auth');
12365
12448
  }
12366
12449
  finally {
12367
12450
  setLoading(false);
12368
12451
  }
12369
12452
  };
12370
- const handleMagicLink = async (email) => {
12371
- setLoading(true);
12372
- setError(undefined);
12373
- try {
12374
- await api.sendMagicLink(email, getRedirectUrl());
12375
- setAuthSuccess(true);
12376
- setSuccessMessage('Magic link sent! Check your email to log in.');
12453
+ const handleFieldChange = (fieldName, value) => {
12454
+ setClaimData(prev => ({
12455
+ ...prev,
12456
+ [fieldName]: value,
12457
+ }));
12458
+ };
12459
+ // Render authentication step
12460
+ if (claimStep === 'auth') {
12461
+ return (jsx("div", { className: className, children: jsx(SmartlinksAuthUI, { apiEndpoint: apiEndpoint, clientId: clientId, clientName: clientName, onAuthSuccess: handleAuthSuccess, onAuthError: onClaimError, theme: theme, minimal: minimal, customization: customization.authConfig }) }));
12462
+ }
12463
+ // Render additional questions step
12464
+ if (claimStep === 'questions') {
12465
+ return (jsxs("div", { className: `claim-questions ${className}`, children: [jsxs("div", { className: "claim-header mb-6", children: [jsx("h2", { className: "text-2xl font-bold mb-2", children: customization.claimTitle || 'Complete Your Claim' }), customization.claimDescription && (jsx("p", { className: "text-muted-foreground", children: customization.claimDescription }))] }), error && (jsx("div", { className: "claim-error bg-destructive/10 text-destructive px-4 py-3 rounded-md mb-4", children: error })), jsxs("form", { onSubmit: handleQuestionSubmit, className: "claim-form space-y-4", children: [additionalFields.map((field) => (jsxs("div", { className: "claim-field", children: [jsxs("label", { htmlFor: field.name, className: "block text-sm font-medium mb-2", children: [field.label, field.required && jsx("span", { className: "text-destructive ml-1", children: "*" })] }), field.type === 'textarea' ? (jsx("textarea", { id: field.name, name: field.name, placeholder: field.placeholder, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background", rows: 4 })) : field.type === 'select' ? (jsxs("select", { id: field.name, name: field.name, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background", children: [jsx("option", { value: "", children: "Select..." }), field.options?.map((option) => (jsx("option", { value: option, children: option }, option)))] })) : (jsx("input", { id: field.name, name: field.name, type: field.type, placeholder: field.placeholder, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background" }))] }, field.name))), jsx("button", { type: "submit", disabled: loading, className: "claim-submit-button w-full bg-primary text-primary-foreground px-4 py-2 rounded-md font-medium hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed", children: loading ? 'Claiming...' : 'Submit Claim' })] })] }));
12466
+ }
12467
+ // Render claiming step (loading state)
12468
+ if (claimStep === 'claiming') {
12469
+ return (jsxs("div", { className: `claim-loading ${className} flex flex-col items-center justify-center py-12`, children: [jsx("div", { className: "claim-spinner w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin mb-4" }), jsx("p", { className: "text-muted-foreground", children: "Claiming your product..." })] }));
12470
+ }
12471
+ // Render success step
12472
+ if (claimStep === 'success') {
12473
+ return (jsxs("div", { className: `claim-success ${className} text-center py-12`, children: [jsx("div", { className: "claim-success-icon w-16 h-16 bg-green-500 text-white rounded-full flex items-center justify-center text-3xl font-bold mx-auto mb-4", children: "\u2713" }), jsx("h2", { className: "text-2xl font-bold mb-2", children: "Claim Successful!" }), jsx("p", { className: "text-muted-foreground", children: customization.successMessage || 'Your product has been successfully claimed and registered to your account.' })] }));
12474
+ }
12475
+ return null;
12476
+ };
12477
+
12478
+ const ProtectedRoute = ({ children, fallback, redirectTo, }) => {
12479
+ const { isAuthenticated, isLoading } = useAuth();
12480
+ // Show loading state
12481
+ if (isLoading) {
12482
+ return jsx("div", { children: "Loading..." });
12483
+ }
12484
+ // If not authenticated, redirect or show fallback
12485
+ if (!isAuthenticated) {
12486
+ if (redirectTo) {
12487
+ window.location.href = redirectTo;
12488
+ return null;
12377
12489
  }
12378
- catch (err) {
12379
- const errorMessage = err instanceof Error ? err.message : 'Failed to send magic link';
12380
- setError(errorMessage);
12381
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
12490
+ return fallback ? jsx(Fragment, { children: fallback }) : jsx("div", { children: "Access denied. Please log in." });
12491
+ }
12492
+ // Render protected content
12493
+ return jsx(Fragment, { children: children });
12494
+ };
12495
+
12496
+ const AuthUIPreview = ({ customization, enabledProviders = ['email', 'google', 'phone'], providerOrder, emailDisplayMode = 'form', theme = 'light', className, minimal = false, }) => {
12497
+ const showEmail = enabledProviders.includes('email');
12498
+ const showGoogle = enabledProviders.includes('google');
12499
+ const showPhone = enabledProviders.includes('phone');
12500
+ const showMagicLink = enabledProviders.includes('magic-link');
12501
+ // Determine ordered providers (excluding email if in button mode)
12502
+ const orderedProviders = providerOrder && providerOrder.length > 0
12503
+ ? providerOrder.filter(p => enabledProviders.includes(p) && p !== 'email')
12504
+ : enabledProviders.filter(p => p !== 'email');
12505
+ const hasOtherProviders = showGoogle || showPhone || showMagicLink;
12506
+ // Render provider button helper
12507
+ const renderProviderButton = (provider) => {
12508
+ if (provider === 'google' && showGoogle) {
12509
+ return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsxs("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "none", children: [jsx("path", { d: "M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z", fill: "#4285F4" }), jsx("path", { d: "M9 18c2.43 0 4.467-.806 5.956-2.183l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332C2.438 15.983 5.482 18 9 18z", fill: "#34A853" }), jsx("path", { d: "M3.964 10.707c-.18-.54-.282-1.117-.282-1.707 0-.593.102-1.167.282-1.707V4.961H.957C.347 6.175 0 7.548 0 9s.348 2.825.957 4.039l3.007-2.332z", fill: "#FBBC05" }), jsx("path", { d: "M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0 5.482 0 2.438 2.017.957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z", fill: "#EA4335" })] }), jsx("span", { children: "Continue with Google" })] }, "google"));
12382
12510
  }
12383
- finally {
12384
- setLoading(false);
12511
+ if (provider === 'phone' && showPhone) {
12512
+ return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsx("path", { d: "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" }) }), jsx("span", { children: "Continue with Phone" })] }, "phone"));
12513
+ }
12514
+ if (provider === 'magic-link' && showMagicLink) {
12515
+ return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }), jsx("span", { children: "Continue with Magic Link" })] }, "magic-link"));
12385
12516
  }
12517
+ if (provider === 'email' && showEmail && emailDisplayMode === 'button') {
12518
+ return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }), jsx("span", { children: "Continue with Email" })] }, "email"));
12519
+ }
12520
+ return null;
12386
12521
  };
12387
- if (configLoading) {
12388
- return (jsx(AuthContainer, { theme: theme, className: className, minimal: minimal || config?.branding?.minimal || false, children: jsx("div", { style: { textAlign: 'center', padding: '2rem' }, children: jsx("div", { className: "auth-spinner" }) }) }));
12389
- }
12390
- return (jsx(AuthContainer, { theme: theme, className: className, config: config, minimal: minimal || config?.branding?.minimal || false, children: authSuccess ? (jsxs("div", { style: { textAlign: 'center', padding: '2rem' }, children: [jsx("div", { style: {
12391
- color: 'var(--auth-primary-color, #4F46E5)',
12392
- fontSize: '3rem',
12393
- marginBottom: '1rem'
12394
- }, children: "\u2713" }), jsx("h2", { style: {
12395
- marginBottom: '0.5rem',
12396
- fontSize: '1.5rem',
12397
- fontWeight: 600
12398
- }, children: successMessage?.includes('verified') ? 'Email Verified!' :
12399
- successMessage?.includes('Magic link') ? 'Check Your Email!' :
12400
- mode === 'register' ? 'Account Created!' : 'Login Successful!' }), jsx("p", { style: {
12401
- color: '#6B7280',
12402
- fontSize: '0.875rem'
12403
- }, children: successMessage })] })) : mode === 'magic-link' ? (jsx(MagicLinkForm, { onSubmit: handleMagicLink, onCancel: () => setMode('login'), loading: loading, error: error })) : mode === 'phone' ? (jsx(PhoneAuthForm, { onSubmit: handlePhoneAuth, onBack: () => setMode('login'), loading: loading, error: error })) : mode === 'reset-password' ? (jsx(PasswordResetForm, { onSubmit: handlePasswordReset, onBack: () => {
12404
- setMode('login');
12405
- setResetSuccess(false);
12406
- setResetToken(undefined); // Clear token when going back
12407
- }, loading: loading, error: error, success: resetSuccess, token: resetToken })) : (mode === 'login' || mode === 'register') ? (jsx(Fragment, { children: showResendVerification ? (jsxs("div", { style: { marginTop: '1rem', padding: '1.5rem', backgroundColor: 'rgba(79, 70, 229, 0.05)', borderRadius: '0.5rem' }, children: [jsx("h3", { style: { marginBottom: '0.75rem', fontSize: '1rem', fontWeight: 600, color: 'var(--auth-text-color, #374151)' }, children: "Verification Link Expired" }), jsx("p", { style: { marginBottom: '1rem', fontSize: '0.875rem', color: 'var(--auth-text-color, #6B7280)', lineHeight: '1.5' }, children: "Your verification link has expired or is no longer valid. Please enter your email address below and we'll send you a new verification link." }), jsx("input", { type: "email", value: resendEmail || '', onChange: (e) => setResendEmail(e.target.value), placeholder: "your@email.com", style: {
12408
- width: '100%',
12409
- padding: '0.625rem',
12410
- marginBottom: '1rem',
12411
- border: '1px solid var(--auth-border-color, #D1D5DB)',
12412
- borderRadius: '0.375rem',
12413
- fontSize: '0.875rem',
12414
- boxSizing: 'border-box'
12415
- } }), jsxs("div", { style: { display: 'flex', gap: '0.75rem' }, children: [jsx("button", { onClick: handleResendVerification, disabled: loading || !resendEmail, style: {
12416
- flex: 1,
12417
- padding: '0.625rem 1rem',
12418
- backgroundColor: 'var(--auth-primary-color, #4F46E5)',
12419
- color: 'white',
12420
- border: 'none',
12421
- borderRadius: '0.375rem',
12422
- cursor: (loading || !resendEmail) ? 'not-allowed' : 'pointer',
12423
- fontSize: '0.875rem',
12424
- fontWeight: 500,
12425
- opacity: (loading || !resendEmail) ? 0.6 : 1
12426
- }, children: loading ? 'Sending...' : 'Send New Verification Link' }), jsx("button", { onClick: () => {
12427
- setShowResendVerification(false);
12428
- setResendEmail('');
12429
- setError(undefined);
12430
- }, disabled: loading, style: {
12431
- padding: '0.625rem 1rem',
12432
- backgroundColor: 'transparent',
12433
- color: 'var(--auth-text-color, #6B7280)',
12434
- border: '1px solid var(--auth-border-color, #D1D5DB)',
12435
- borderRadius: '0.375rem',
12436
- cursor: loading ? 'not-allowed' : 'pointer',
12437
- fontSize: '0.875rem',
12438
- fontWeight: 500,
12439
- opacity: loading ? 0.6 : 1
12440
- }, children: "Cancel" })] })] })) : showRequestNewReset ? (jsxs("div", { style: { marginTop: '1rem', padding: '1.5rem', backgroundColor: 'rgba(239, 68, 68, 0.05)', borderRadius: '0.5rem' }, children: [jsx("h3", { style: { marginBottom: '0.75rem', fontSize: '1rem', fontWeight: 600, color: 'var(--auth-text-color, #374151)' }, children: "Password Reset Link Expired" }), jsx("p", { style: { marginBottom: '1rem', fontSize: '0.875rem', color: 'var(--auth-text-color, #6B7280)', lineHeight: '1.5' }, children: "Your password reset link has expired or is no longer valid. Please enter your email address below and we'll send you a new password reset link." }), jsx("input", { type: "email", value: resetRequestEmail || '', onChange: (e) => setResetRequestEmail(e.target.value), placeholder: "your@email.com", style: {
12441
- width: '100%',
12442
- padding: '0.625rem',
12443
- marginBottom: '1rem',
12444
- border: '1px solid var(--auth-border-color, #D1D5DB)',
12445
- borderRadius: '0.375rem',
12446
- fontSize: '0.875rem',
12447
- boxSizing: 'border-box'
12448
- } }), jsxs("div", { style: { display: 'flex', gap: '0.75rem' }, children: [jsx("button", { onClick: handleRequestNewReset, disabled: loading || !resetRequestEmail, style: {
12449
- flex: 1,
12450
- padding: '0.625rem 1rem',
12451
- backgroundColor: '#EF4444',
12452
- color: 'white',
12453
- border: 'none',
12454
- borderRadius: '0.375rem',
12455
- cursor: (loading || !resetRequestEmail) ? 'not-allowed' : 'pointer',
12456
- fontSize: '0.875rem',
12457
- fontWeight: 500,
12458
- opacity: (loading || !resetRequestEmail) ? 0.6 : 1
12459
- }, children: loading ? 'Sending...' : 'Send New Reset Link' }), jsx("button", { onClick: () => {
12460
- setShowRequestNewReset(false);
12461
- setResetRequestEmail('');
12462
- setError(undefined);
12463
- }, disabled: loading, style: {
12464
- padding: '0.625rem 1rem',
12465
- backgroundColor: 'transparent',
12466
- color: 'var(--auth-text-color, #6B7280)',
12467
- border: '1px solid var(--auth-border-color, #D1D5DB)',
12468
- borderRadius: '0.375rem',
12469
- cursor: loading ? 'not-allowed' : 'pointer',
12470
- fontSize: '0.875rem',
12471
- fontWeight: 500,
12472
- opacity: loading ? 0.6 : 1
12473
- }, children: "Cancel" })] })] })) : (jsx(Fragment, { children: (() => {
12474
- const emailDisplayMode = config?.emailDisplayMode || 'form';
12475
- const providerOrder = config?.providerOrder || (config?.enabledProviders || enabledProviders);
12476
- const actualProviders = config?.enabledProviders || enabledProviders;
12477
- // Button mode: show provider selection first, then email form if email is selected
12478
- if (emailDisplayMode === 'button' && !showEmailForm) {
12479
- return (jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onEmailLogin: () => setShowEmailForm(true), onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }));
12480
- }
12481
- // Form mode or email button was clicked: show email form with other providers
12482
- return (jsxs(Fragment, { children: [emailDisplayMode === 'button' && showEmailForm && (jsx("button", { onClick: () => setShowEmailForm(false), style: {
12483
- marginBottom: '1rem',
12484
- padding: '0.5rem',
12485
- background: 'none',
12486
- border: 'none',
12487
- color: 'var(--auth-text-color, #6B7280)',
12488
- cursor: 'pointer',
12489
- fontSize: '0.875rem',
12490
- display: 'flex',
12491
- alignItems: 'center',
12492
- gap: '0.25rem'
12493
- }, children: "\u2190 Back to options" })), jsx(EmailAuthForm, { mode: mode, onSubmit: handleEmailAuth, onModeSwitch: () => {
12494
- setMode(mode === 'login' ? 'register' : 'login');
12495
- setShowResendVerification(false);
12496
- setShowRequestNewReset(false);
12497
- setError(undefined);
12498
- }, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error, additionalFields: config?.signupAdditionalFields }), emailDisplayMode === 'form' && actualProviders.length > 1 && (jsx(ProviderButtons, { enabledProviders: actualProviders.filter((p) => p !== 'email'), providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }))] }));
12499
- })() })) })) : null }));
12522
+ return (jsx(AuthContainer, { theme: theme, className: className, config: customization, minimal: minimal, children: emailDisplayMode === 'button' ? (jsx("div", { className: "auth-provider-buttons", children: orderedProviders.concat(showEmail ? ['email'] : []).map(provider => renderProviderButton(provider)) })) : (
12523
+ /* Form mode: show email form first, then other providers */
12524
+ jsxs(Fragment, { children: [showEmail && (jsxs("div", { className: "auth-form", children: [jsxs("div", { className: "auth-form-group", children: [jsx("label", { className: "auth-label", children: "Email" }), jsx("input", { type: "email", className: "auth-input", placeholder: "Enter your email", disabled: true })] }), jsxs("div", { className: "auth-form-group", children: [jsx("label", { className: "auth-label", children: "Password" }), jsx("input", { type: "password", className: "auth-input", placeholder: "Enter your password", disabled: true })] }), jsx("button", { className: "auth-button auth-button-primary", disabled: true, children: "Sign In" }), jsx("div", { style: { textAlign: 'center', marginTop: '1rem' }, children: jsx("button", { className: "auth-link", disabled: true, children: "Forgot password?" }) }), jsxs("div", { style: { textAlign: 'center', marginTop: '0.5rem', fontSize: '0.875rem', color: 'var(--auth-text-muted, #6B7280)' }, children: ["Don't have an account?", ' ', jsx("button", { className: "auth-link", disabled: true, children: "Sign up" })] })] })), hasOtherProviders && (jsxs(Fragment, { children: [showEmail && (jsx("div", { className: "auth-or-divider", children: jsx("span", { children: "or continue with" }) })), jsx("div", { className: "auth-provider-buttons", children: orderedProviders.map(provider => renderProviderButton(provider)) })] }))] })) }));
12500
12525
  };
12501
12526
 
12502
12527
  export { AccountManagement, AuthProvider, AuthUIPreview, SmartlinksAuthUI as FirebaseAuthUI, ProtectedRoute, SmartlinksAuthUI, SmartlinksClaimUI, tokenStorage, useAuth };