@proveanything/smartlinks-auth-ui 0.1.10 → 0.1.13

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
@@ -10646,8 +10646,21 @@ class AuthAPI {
10646
10646
  accountData: data.accountData,
10647
10647
  });
10648
10648
  }
10649
- async loginWithGoogle(idToken) {
10650
- return smartlinks.authKit.googleLogin(this.clientId, idToken);
10649
+ async loginWithGoogle(token, options) {
10650
+ this.log.log('loginWithGoogle called:', {
10651
+ tokenType: options?.tokenType || 'id_token',
10652
+ hasUserInfo: !!options?.googleUserInfo,
10653
+ userEmail: options?.googleUserInfo?.email,
10654
+ tokenLength: token?.length,
10655
+ });
10656
+ // Note: The SDK only supports ID tokens currently
10657
+ // Access tokens from popup flow may fail with "Invalid or expired Google token"
10658
+ if (options?.tokenType === 'access_token') {
10659
+ this.log.warn('Warning: Popup flow sends access_token, but backend expects id_token. This may fail.');
10660
+ this.log.warn('Consider using OneTap flow (default) or updating backend to handle access tokens.');
10661
+ }
10662
+ // Pass token to SDK - backend verifies with Google
10663
+ return smartlinks.authKit.googleLogin(this.clientId, token);
10651
10664
  }
10652
10665
  async sendPhoneCode(phoneNumber) {
10653
10666
  return smartlinks.authKit.sendPhoneCode(this.clientId, phoneNumber);
@@ -11204,7 +11217,7 @@ const tokenStorage = {
11204
11217
  };
11205
11218
 
11206
11219
  const AuthContext = createContext(undefined);
11207
- const AuthProvider = ({ children, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false }) => {
11220
+ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false }) => {
11208
11221
  const [user, setUser] = useState(null);
11209
11222
  const [token, setToken] = useState(null);
11210
11223
  const [accountData, setAccountData] = useState(null);
@@ -11228,10 +11241,45 @@ const AuthProvider = ({ children, accountCacheTTL = 5 * 60 * 1000, preloadAccoun
11228
11241
  }
11229
11242
  });
11230
11243
  }, []);
11231
- // Initialize auth state from persistent storage
11244
+ // Initialize auth state - different behavior for proxy mode vs standalone mode
11232
11245
  useEffect(() => {
11233
11246
  const initializeAuth = async () => {
11234
11247
  try {
11248
+ if (proxyMode) {
11249
+ // PROXY MODE: Initialize from URL params and parent via SDK
11250
+ const params = new URLSearchParams(window.location.search);
11251
+ const userId = params.get('userId');
11252
+ if (userId) {
11253
+ console.log('[AuthContext] Proxy mode: userId detected, fetching account from parent');
11254
+ try {
11255
+ // Fetch account details from parent via proxied API call
11256
+ const accountResponse = await smartlinks.auth.getAccount();
11257
+ // Build user object from account response
11258
+ const accountAny = accountResponse;
11259
+ const userFromAccount = {
11260
+ uid: userId,
11261
+ email: accountAny?.email,
11262
+ displayName: accountAny?.displayName || accountAny?.name,
11263
+ phoneNumber: accountAny?.phoneNumber,
11264
+ };
11265
+ setUser(userFromAccount);
11266
+ setAccountData(accountResponse);
11267
+ setAccountInfo(accountResponse);
11268
+ console.log('[AuthContext] Proxy mode: initialized from parent account');
11269
+ notifyAuthStateChange('LOGIN', userFromAccount, null, accountResponse, accountResponse);
11270
+ }
11271
+ catch (error) {
11272
+ console.warn('[AuthContext] Proxy mode: failed to fetch account from parent:', error);
11273
+ // No session - that's ok, user may need to login
11274
+ }
11275
+ }
11276
+ else {
11277
+ console.log('[AuthContext] Proxy mode: no userId in URL, awaiting login');
11278
+ }
11279
+ setIsLoading(false);
11280
+ return;
11281
+ }
11282
+ // STANDALONE MODE: Load from persistent storage
11235
11283
  const storedToken = await tokenStorage.getToken();
11236
11284
  const storedUser = await tokenStorage.getUser();
11237
11285
  const storedAccountData = await tokenStorage.getAccountData();
@@ -11258,9 +11306,49 @@ const AuthProvider = ({ children, accountCacheTTL = 5 * 60 * 1000, preloadAccoun
11258
11306
  }
11259
11307
  };
11260
11308
  initializeAuth();
11261
- }, []);
11262
- // Cross-tab synchronization - listen for auth changes in other tabs
11309
+ }, [proxyMode, notifyAuthStateChange]);
11310
+ // Listen for parent auth state changes (proxy mode only)
11311
+ useEffect(() => {
11312
+ if (!proxyMode)
11313
+ return;
11314
+ console.log('[AuthContext] Proxy mode: setting up parent message listener');
11315
+ const handleParentMessage = (event) => {
11316
+ // Handle auth state pushed from parent
11317
+ if (event.data?.type === 'smartlinks:authkit:state') {
11318
+ const { user: parentUser, accountData: parentAccountData, authenticated } = event.data.payload || {};
11319
+ console.log('[AuthContext] Proxy mode: received state from parent:', { authenticated });
11320
+ if (authenticated && parentUser) {
11321
+ const userObj = {
11322
+ uid: parentUser.uid || parentUser.id,
11323
+ email: parentUser.email,
11324
+ displayName: parentUser.displayName || parentUser.name,
11325
+ phoneNumber: parentUser.phoneNumber,
11326
+ };
11327
+ setUser(userObj);
11328
+ setAccountData(parentAccountData || null);
11329
+ setAccountInfo(parentAccountData || null);
11330
+ notifyAuthStateChange('CROSS_TAB_SYNC', userObj, null, parentAccountData || null, parentAccountData || null);
11331
+ }
11332
+ else {
11333
+ // Parent indicates no session / logged out
11334
+ setUser(null);
11335
+ setToken(null);
11336
+ setAccountData(null);
11337
+ setAccountInfo(null);
11338
+ notifyAuthStateChange('LOGOUT', null, null, null, null);
11339
+ }
11340
+ }
11341
+ };
11342
+ window.addEventListener('message', handleParentMessage);
11343
+ return () => {
11344
+ console.log('[AuthContext] Proxy mode: cleaning up parent message listener');
11345
+ window.removeEventListener('message', handleParentMessage);
11346
+ };
11347
+ }, [proxyMode, notifyAuthStateChange]);
11348
+ // Cross-tab synchronization - standalone mode only
11263
11349
  useEffect(() => {
11350
+ if (proxyMode)
11351
+ return; // Skip cross-tab sync in proxy mode
11264
11352
  console.log('[AuthContext] Setting up cross-tab synchronization');
11265
11353
  const unsubscribe = onStorageChange(async (event) => {
11266
11354
  console.log('[AuthContext] Cross-tab storage event:', event.type, event.key);
@@ -11319,27 +11407,38 @@ const AuthProvider = ({ children, accountCacheTTL = 5 * 60 * 1000, preloadAccoun
11319
11407
  console.log('[AuthContext] Cleaning up cross-tab synchronization');
11320
11408
  unsubscribe();
11321
11409
  };
11322
- }, [notifyAuthStateChange]);
11410
+ }, [proxyMode, notifyAuthStateChange]);
11323
11411
  const login = useCallback(async (authToken, authUser, authAccountData) => {
11324
11412
  try {
11325
- // Store token, user, and account data
11326
- await tokenStorage.saveToken(authToken);
11327
- await tokenStorage.saveUser(authUser);
11328
- if (authAccountData) {
11329
- await tokenStorage.saveAccountData(authAccountData);
11413
+ // Only persist to storage in standalone mode
11414
+ if (!proxyMode) {
11415
+ await tokenStorage.saveToken(authToken);
11416
+ await tokenStorage.saveUser(authUser);
11417
+ if (authAccountData) {
11418
+ await tokenStorage.saveAccountData(authAccountData);
11419
+ }
11420
+ // Set bearer token in global Smartlinks SDK via auth.verifyToken
11421
+ smartlinks.auth.verifyToken(authToken).catch(err => {
11422
+ console.warn('Failed to set bearer token on login:', err);
11423
+ });
11330
11424
  }
11425
+ // Always update memory state
11331
11426
  setToken(authToken);
11332
11427
  setUser(authUser);
11333
11428
  setAccountData(authAccountData || null);
11334
- // Set bearer token in global Smartlinks SDK via auth.verifyToken
11335
- // This both validates the token and sets it for future API calls
11336
- smartlinks.auth.verifyToken(authToken).catch(err => {
11337
- console.warn('Failed to set bearer token on login:', err);
11338
- });
11429
+ // Cross-iframe auth state synchronization
11430
+ // Always notify parent frame of login (both modes, but especially important in proxy mode)
11431
+ const sdk = smartlinks;
11432
+ if (sdk.iframe?.isIframe?.()) {
11433
+ sdk.iframe.sendParentCustom('smartlinks:authkit:login', {
11434
+ token: authToken,
11435
+ user: authUser,
11436
+ accountData: authAccountData || null
11437
+ });
11438
+ }
11339
11439
  notifyAuthStateChange('LOGIN', authUser, authToken, authAccountData || null);
11340
- // Optionally preload account info on login
11341
- if (preloadAccountInfo) {
11342
- // Preload after login completes (non-blocking)
11440
+ // Optionally preload account info on login (standalone mode only)
11441
+ if (!proxyMode && preloadAccountInfo) {
11343
11442
  getAccount(true).catch(error => {
11344
11443
  console.warn('[AuthContext] Failed to preload account info:', error);
11345
11444
  });
@@ -11349,34 +11448,55 @@ const AuthProvider = ({ children, accountCacheTTL = 5 * 60 * 1000, preloadAccoun
11349
11448
  console.error('Failed to save auth data to storage:', error);
11350
11449
  throw error;
11351
11450
  }
11352
- }, [notifyAuthStateChange, preloadAccountInfo]);
11451
+ }, [proxyMode, notifyAuthStateChange, preloadAccountInfo]);
11353
11452
  const logout = useCallback(async () => {
11354
11453
  try {
11355
- // Clear persistent storage
11356
- await tokenStorage.clearAll();
11454
+ // Only clear persistent storage in standalone mode
11455
+ if (!proxyMode) {
11456
+ await tokenStorage.clearAll();
11457
+ smartlinks.auth.logout();
11458
+ }
11459
+ // Always clear memory state
11357
11460
  setToken(null);
11358
11461
  setUser(null);
11359
11462
  setAccountData(null);
11360
11463
  setAccountInfo(null);
11361
- // Clear bearer token from global Smartlinks SDK
11362
- smartlinks.auth.logout();
11464
+ // Cross-iframe auth state synchronization
11465
+ // Always notify parent frame of logout
11466
+ const sdk = smartlinks;
11467
+ if (sdk.iframe?.isIframe?.()) {
11468
+ sdk.iframe.sendParentCustom('smartlinks:authkit:logout', {});
11469
+ }
11363
11470
  notifyAuthStateChange('LOGOUT', null, null, null);
11364
11471
  }
11365
11472
  catch (error) {
11366
11473
  console.error('Failed to clear auth data from storage:', error);
11367
11474
  }
11368
- }, [notifyAuthStateChange]);
11475
+ }, [proxyMode, notifyAuthStateChange]);
11369
11476
  const getToken = useCallback(async () => {
11477
+ if (proxyMode) {
11478
+ // In proxy mode, token is managed by parent - return memory state
11479
+ return token;
11480
+ }
11370
11481
  const storedToken = await tokenStorage.getToken();
11371
11482
  return storedToken ? storedToken.token : null;
11372
- }, []);
11483
+ }, [proxyMode, token]);
11373
11484
  const refreshToken = useCallback(async () => {
11374
11485
  throw new Error('Token refresh must be implemented via your backend API');
11375
11486
  }, []);
11376
- // Get account with intelligent caching
11487
+ // Get account with intelligent caching (or direct parent fetch in proxy mode)
11377
11488
  const getAccount = useCallback(async (forceRefresh = false) => {
11378
11489
  try {
11379
- // Check if user is authenticated
11490
+ if (proxyMode) {
11491
+ // PROXY MODE: Always fetch from parent via proxied API, no local cache
11492
+ console.log('[AuthContext] Proxy mode: fetching account from parent');
11493
+ const freshAccountInfo = await smartlinks.auth.getAccount();
11494
+ setAccountInfo(freshAccountInfo);
11495
+ setAccountData(freshAccountInfo);
11496
+ notifyAuthStateChange('ACCOUNT_REFRESH', user, token, freshAccountInfo, freshAccountInfo);
11497
+ return freshAccountInfo;
11498
+ }
11499
+ // STANDALONE MODE: Use caching
11380
11500
  if (!token) {
11381
11501
  throw new Error('Not authenticated. Please login first.');
11382
11502
  }
@@ -11399,24 +11519,28 @@ const AuthProvider = ({ children, accountCacheTTL = 5 * 60 * 1000, preloadAccoun
11399
11519
  }
11400
11520
  catch (error) {
11401
11521
  console.error('[AuthContext] Failed to get account info:', error);
11402
- // Fallback to stale cache if API fails
11403
- const cached = await tokenStorage.getAccountInfo();
11404
- if (cached) {
11405
- console.warn('[AuthContext] Returning stale cached data due to API error');
11406
- return cached.data;
11522
+ // Fallback to stale cache if API fails (standalone mode only)
11523
+ if (!proxyMode) {
11524
+ const cached = await tokenStorage.getAccountInfo();
11525
+ if (cached) {
11526
+ console.warn('[AuthContext] Returning stale cached data due to API error');
11527
+ return cached.data;
11528
+ }
11407
11529
  }
11408
11530
  throw error;
11409
11531
  }
11410
- }, [token, accountCacheTTL, user, accountData, notifyAuthStateChange]);
11532
+ }, [proxyMode, token, accountCacheTTL, user, accountData, notifyAuthStateChange]);
11411
11533
  // Convenience method for explicit refresh
11412
11534
  const refreshAccount = useCallback(async () => {
11413
11535
  return await getAccount(true);
11414
11536
  }, [getAccount]);
11415
- // Clear account cache
11537
+ // Clear account cache (no-op in proxy mode)
11416
11538
  const clearAccountCache = useCallback(async () => {
11417
- await tokenStorage.clearAccountInfo();
11539
+ if (!proxyMode) {
11540
+ await tokenStorage.clearAccountInfo();
11541
+ }
11418
11542
  setAccountInfo(null);
11419
- }, []);
11543
+ }, [proxyMode]);
11420
11544
  const onAuthStateChange = useCallback((callback) => {
11421
11545
  callbacksRef.current.add(callback);
11422
11546
  // Return unsubscribe function
@@ -11429,8 +11553,9 @@ const AuthProvider = ({ children, accountCacheTTL = 5 * 60 * 1000, preloadAccoun
11429
11553
  token,
11430
11554
  accountData,
11431
11555
  accountInfo,
11432
- isAuthenticated: !!token && !!user,
11556
+ isAuthenticated: !!user,
11433
11557
  isLoading,
11558
+ proxyMode,
11434
11559
  login,
11435
11560
  logout,
11436
11561
  getToken,
@@ -11493,7 +11618,34 @@ const loadGoogleIdentityServices = () => {
11493
11618
  document.head.appendChild(script);
11494
11619
  });
11495
11620
  };
11496
- const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, enabledProviders = ['email', 'google', 'phone'], initialMode = 'login', redirectUrl, theme = 'auto', className, customization, skipConfigFetch = false, minimal = false, logger, }) => {
11621
+ // Helper to convert generic SDK errors to user-friendly messages
11622
+ const getFriendlyErrorMessage = (errorMessage) => {
11623
+ // Check for common HTTP status codes in the error message
11624
+ if (errorMessage.includes('status 400')) {
11625
+ return 'Invalid request. Please check your input and try again.';
11626
+ }
11627
+ if (errorMessage.includes('status 401')) {
11628
+ return 'Invalid credentials. Please check your email and password.';
11629
+ }
11630
+ if (errorMessage.includes('status 403')) {
11631
+ return 'Access denied. You do not have permission to perform this action.';
11632
+ }
11633
+ if (errorMessage.includes('status 404')) {
11634
+ return 'Account not found. Please check your email or create a new account.';
11635
+ }
11636
+ if (errorMessage.includes('status 409')) {
11637
+ return 'This email is already registered.';
11638
+ }
11639
+ if (errorMessage.includes('status 429')) {
11640
+ return 'Too many attempts. Please wait a moment and try again.';
11641
+ }
11642
+ if (errorMessage.includes('status 500') || errorMessage.includes('status 502') || errorMessage.includes('status 503')) {
11643
+ return 'Server error. Please try again later.';
11644
+ }
11645
+ // Return original message if no pattern matches
11646
+ return errorMessage;
11647
+ };
11648
+ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, enabledProviders = ['email', 'google', 'phone'], initialMode = 'login', redirectUrl, theme = 'auto', className, customization, skipConfigFetch = false, minimal = false, logger, proxyMode = false, }) => {
11497
11649
  const [mode, setMode] = useState(initialMode);
11498
11650
  const [loading, setLoading] = useState(false);
11499
11651
  const [error, setError] = useState();
@@ -11526,25 +11678,25 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11526
11678
  mediaQuery.addEventListener('change', updateTheme);
11527
11679
  return () => mediaQuery.removeEventListener('change', updateTheme);
11528
11680
  }, [theme]);
11529
- // Reinitialize Smartlinks SDK when apiEndpoint changes (for test/dev scenarios)
11681
+ // Reinitialize Smartlinks SDK when apiEndpoint or proxyMode changes
11530
11682
  // IMPORTANT: Preserve bearer token during reinitialization
11531
11683
  useEffect(() => {
11532
- log.log('SDK reinitialize useEffect triggered', { apiEndpoint });
11684
+ log.log('SDK reinitialize useEffect triggered', { apiEndpoint, proxyMode });
11533
11685
  setSdkReady(false); // Mark SDK as not ready during reinitialization
11534
11686
  const reinitializeWithToken = async () => {
11535
11687
  if (apiEndpoint) {
11536
- log.log('Reinitializing SDK with baseURL:', apiEndpoint);
11537
- // Get current token before reinitializing
11538
- const currentToken = await auth.getToken();
11688
+ log.log('Reinitializing SDK with baseURL:', apiEndpoint, 'proxyMode:', proxyMode);
11689
+ // Get current token before reinitializing (only in standalone mode)
11690
+ const currentToken = !proxyMode ? await auth.getToken() : null;
11539
11691
  smartlinks.initializeApi({
11540
11692
  baseURL: apiEndpoint,
11541
- proxyMode: false, // Direct API calls when custom endpoint is provided
11693
+ proxyMode: proxyMode, // Use prop value
11542
11694
  ngrokSkipBrowserWarning: true,
11543
11695
  logger: logger, // Pass logger to SDK for verbose SDK logging
11544
11696
  });
11545
11697
  log.log('SDK reinitialized successfully');
11546
- // Restore bearer token after reinitialization using auth.verifyToken
11547
- if (currentToken) {
11698
+ // Restore bearer token after reinitialization using auth.verifyToken (standalone mode only)
11699
+ if (currentToken && !proxyMode) {
11548
11700
  smartlinks.auth.verifyToken(currentToken).catch(err => {
11549
11701
  log.warn('Failed to restore bearer token after reinit:', err);
11550
11702
  });
@@ -11552,6 +11704,11 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11552
11704
  // Mark SDK as ready
11553
11705
  setSdkReady(true);
11554
11706
  }
11707
+ else if (proxyMode) {
11708
+ // In proxy mode without custom endpoint, SDK should already be initialized by parent
11709
+ log.log('Proxy mode without apiEndpoint, SDK already initialized by parent');
11710
+ setSdkReady(true);
11711
+ }
11555
11712
  else {
11556
11713
  log.log('No apiEndpoint, SDK already initialized by App');
11557
11714
  // SDK was initialized by App component, mark as ready
@@ -11559,7 +11716,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11559
11716
  }
11560
11717
  };
11561
11718
  reinitializeWithToken();
11562
- }, [apiEndpoint, auth, logger, log]);
11719
+ }, [apiEndpoint, proxyMode, auth, logger, log]);
11563
11720
  // Get the effective redirect URL (use prop or default to current page)
11564
11721
  const getRedirectUrl = () => {
11565
11722
  if (redirectUrl)
@@ -11857,14 +12014,18 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11857
12014
  }
11858
12015
  catch (err) {
11859
12016
  const errorMessage = err instanceof Error ? err.message : 'Authentication failed';
11860
- // Check if error is about email already registered
11861
- if (mode === 'register' && errorMessage.toLowerCase().includes('already') && errorMessage.toLowerCase().includes('email')) {
12017
+ // Check if error is about email already registered (by content or 409 status code)
12018
+ const isAlreadyRegistered = mode === 'register' && ((errorMessage.toLowerCase().includes('already') && errorMessage.toLowerCase().includes('email')) ||
12019
+ errorMessage.includes('status 409'));
12020
+ if (isAlreadyRegistered) {
11862
12021
  setShowResendVerification(true);
11863
12022
  setResendEmail(data.email);
11864
12023
  setError('This email is already registered. If you didn\'t receive the verification email, you can resend it below.');
11865
12024
  }
11866
12025
  else {
11867
- setError(errorMessage);
12026
+ // Try to extract a more meaningful error message from status codes
12027
+ const friendlyMessage = getFriendlyErrorMessage(errorMessage);
12028
+ setError(friendlyMessage);
11868
12029
  }
11869
12030
  onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11870
12031
  }
@@ -11920,6 +12081,15 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11920
12081
  const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
11921
12082
  // Determine OAuth flow: default to 'oneTap' for better UX, but allow 'popup' for iframe compatibility
11922
12083
  const oauthFlow = config?.googleOAuthFlow || 'oneTap';
12084
+ // Log Google Auth configuration for debugging
12085
+ log.log('Google Auth initiated:', {
12086
+ googleClientId,
12087
+ oauthFlow,
12088
+ currentOrigin: window.location.origin,
12089
+ currentHref: window.location.href,
12090
+ configGoogleClientId: config?.googleClientId,
12091
+ usingDefaultClientId: !config?.googleClientId,
12092
+ });
11923
12093
  setLoading(true);
11924
12094
  setError(undefined);
11925
12095
  try {
@@ -11929,35 +12099,87 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11929
12099
  if (!google?.accounts) {
11930
12100
  throw new Error('Google Identity Services failed to initialize');
11931
12101
  }
12102
+ log.log('Google Identity Services loaded, using flow:', oauthFlow);
11932
12103
  if (oauthFlow === 'popup') {
11933
12104
  // Use OAuth2 popup flow (works in iframes but requires popup permission)
11934
12105
  if (!google.accounts.oauth2) {
11935
12106
  throw new Error('Google OAuth2 not available');
11936
12107
  }
12108
+ log.log('Initializing Google OAuth2 popup flow:', {
12109
+ client_id: googleClientId,
12110
+ scope: 'openid email profile',
12111
+ origin: window.location.origin,
12112
+ });
11937
12113
  const client = google.accounts.oauth2.initTokenClient({
11938
12114
  client_id: googleClientId,
11939
12115
  scope: 'openid email profile',
11940
12116
  callback: async (response) => {
11941
12117
  try {
12118
+ log.log('Google OAuth2 popup callback received:', {
12119
+ hasAccessToken: !!response.access_token,
12120
+ hasIdToken: !!response.id_token,
12121
+ tokenType: response.token_type,
12122
+ expiresIn: response.expires_in,
12123
+ scope: response.scope,
12124
+ error: response.error,
12125
+ errorDescription: response.error_description,
12126
+ });
11942
12127
  if (response.error) {
11943
12128
  throw new Error(response.error_description || response.error);
11944
12129
  }
12130
+ // OAuth2 popup flow returns access_token, not id_token
12131
+ // We need to use the access token to get user info from Google
11945
12132
  const accessToken = response.access_token;
11946
- // Send access token to backend
11947
- const authResponse = await api.loginWithGoogle(accessToken);
11948
- if (authResponse.token) {
11949
- auth.login(authResponse.token, authResponse.user, authResponse.accountData);
11950
- setAuthSuccess(true);
11951
- setSuccessMessage('Google login successful!');
11952
- onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
12133
+ if (!accessToken) {
12134
+ throw new Error('No access token received from Google');
11953
12135
  }
11954
- else {
11955
- throw new Error('Authentication failed - no token received');
12136
+ log.log('Fetching user info from Google using access token...');
12137
+ // Fetch user info from Google's userinfo endpoint
12138
+ const userInfoResponse = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
12139
+ headers: {
12140
+ 'Authorization': `Bearer ${accessToken}`,
12141
+ },
12142
+ });
12143
+ if (!userInfoResponse.ok) {
12144
+ throw new Error('Failed to fetch user info from Google');
11956
12145
  }
11957
- if (redirectUrl) {
11958
- setTimeout(() => {
11959
- window.location.href = redirectUrl;
11960
- }, 2000);
12146
+ const userInfo = await userInfoResponse.json();
12147
+ log.log('Google user info retrieved:', {
12148
+ email: userInfo.email,
12149
+ name: userInfo.name,
12150
+ sub: userInfo.sub,
12151
+ });
12152
+ // For popup flow, send the access token to backend
12153
+ // Note: This may fail if backend only supports ID token verification
12154
+ try {
12155
+ const authResponse = await api.loginWithGoogle(accessToken, {
12156
+ googleUserInfo: userInfo,
12157
+ tokenType: 'access_token',
12158
+ });
12159
+ if (authResponse.token) {
12160
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData);
12161
+ setAuthSuccess(true);
12162
+ setSuccessMessage('Google login successful!');
12163
+ onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
12164
+ }
12165
+ else {
12166
+ throw new Error('Authentication failed - no token received');
12167
+ }
12168
+ if (redirectUrl) {
12169
+ setTimeout(() => {
12170
+ window.location.href = redirectUrl;
12171
+ }, 2000);
12172
+ }
12173
+ }
12174
+ catch (apiError) {
12175
+ const errorMessage = apiError instanceof Error ? apiError.message : 'Google login failed';
12176
+ // Check if this is the access token vs ID token mismatch
12177
+ if (errorMessage.includes('Invalid or expired Google token')) {
12178
+ log.error('Popup flow access token rejected by backend. Backend may only support ID tokens.');
12179
+ log.error('User info retrieved from Google:', userInfo);
12180
+ throw new Error('Google authentication failed. The popup flow may not be supported. Please try again or contact support.');
12181
+ }
12182
+ throw apiError;
11961
12183
  }
11962
12184
  setLoading(false);
11963
12185
  }
@@ -11973,6 +12195,10 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11973
12195
  }
11974
12196
  else {
11975
12197
  // Use One Tap / Sign-In button flow (smoother UX but doesn't work in iframes)
12198
+ log.log('Initializing Google OneTap flow:', {
12199
+ client_id: googleClientId,
12200
+ origin: window.location.origin,
12201
+ });
11976
12202
  google.accounts.id.initialize({
11977
12203
  client_id: googleClientId,
11978
12204
  callback: async (response) => {
@@ -12004,11 +12230,18 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12004
12230
  },
12005
12231
  auto_select: false,
12006
12232
  cancel_on_tap_outside: true,
12233
+ // Note: use_fedcm_for_prompt omitted - requires Permissions-Policy header on hosting server
12234
+ // Will be needed when FedCM becomes mandatory in the future
12007
12235
  });
12008
- google.accounts.id.prompt((notification) => {
12009
- if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
12010
- setLoading(false);
12011
- }
12236
+ // Use timeout fallback instead of deprecated notification methods
12237
+ // (isNotDisplayed/isSkippedMoment will stop working when FedCM becomes mandatory)
12238
+ const promptTimeout = setTimeout(() => {
12239
+ setLoading(false);
12240
+ }, 5000);
12241
+ google.accounts.id.prompt(() => {
12242
+ // Clear timeout if prompt interaction occurs
12243
+ clearTimeout(promptTimeout);
12244
+ setLoading(false);
12012
12245
  });
12013
12246
  }
12014
12247
  }