@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.js CHANGED
@@ -10667,8 +10667,21 @@ class AuthAPI {
10667
10667
  accountData: data.accountData,
10668
10668
  });
10669
10669
  }
10670
- async loginWithGoogle(idToken) {
10671
- return smartlinks__namespace.authKit.googleLogin(this.clientId, idToken);
10670
+ async loginWithGoogle(token, options) {
10671
+ this.log.log('loginWithGoogle called:', {
10672
+ tokenType: options?.tokenType || 'id_token',
10673
+ hasUserInfo: !!options?.googleUserInfo,
10674
+ userEmail: options?.googleUserInfo?.email,
10675
+ tokenLength: token?.length,
10676
+ });
10677
+ // Note: The SDK only supports ID tokens currently
10678
+ // Access tokens from popup flow may fail with "Invalid or expired Google token"
10679
+ if (options?.tokenType === 'access_token') {
10680
+ this.log.warn('Warning: Popup flow sends access_token, but backend expects id_token. This may fail.');
10681
+ this.log.warn('Consider using OneTap flow (default) or updating backend to handle access tokens.');
10682
+ }
10683
+ // Pass token to SDK - backend verifies with Google
10684
+ return smartlinks__namespace.authKit.googleLogin(this.clientId, token);
10672
10685
  }
10673
10686
  async sendPhoneCode(phoneNumber) {
10674
10687
  return smartlinks__namespace.authKit.sendPhoneCode(this.clientId, phoneNumber);
@@ -11225,7 +11238,7 @@ const tokenStorage = {
11225
11238
  };
11226
11239
 
11227
11240
  const AuthContext = React.createContext(undefined);
11228
- const AuthProvider = ({ children, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false }) => {
11241
+ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false }) => {
11229
11242
  const [user, setUser] = React.useState(null);
11230
11243
  const [token, setToken] = React.useState(null);
11231
11244
  const [accountData, setAccountData] = React.useState(null);
@@ -11249,10 +11262,45 @@ const AuthProvider = ({ children, accountCacheTTL = 5 * 60 * 1000, preloadAccoun
11249
11262
  }
11250
11263
  });
11251
11264
  }, []);
11252
- // Initialize auth state from persistent storage
11265
+ // Initialize auth state - different behavior for proxy mode vs standalone mode
11253
11266
  React.useEffect(() => {
11254
11267
  const initializeAuth = async () => {
11255
11268
  try {
11269
+ if (proxyMode) {
11270
+ // PROXY MODE: Initialize from URL params and parent via SDK
11271
+ const params = new URLSearchParams(window.location.search);
11272
+ const userId = params.get('userId');
11273
+ if (userId) {
11274
+ console.log('[AuthContext] Proxy mode: userId detected, fetching account from parent');
11275
+ try {
11276
+ // Fetch account details from parent via proxied API call
11277
+ const accountResponse = await smartlinks__namespace.auth.getAccount();
11278
+ // Build user object from account response
11279
+ const accountAny = accountResponse;
11280
+ const userFromAccount = {
11281
+ uid: userId,
11282
+ email: accountAny?.email,
11283
+ displayName: accountAny?.displayName || accountAny?.name,
11284
+ phoneNumber: accountAny?.phoneNumber,
11285
+ };
11286
+ setUser(userFromAccount);
11287
+ setAccountData(accountResponse);
11288
+ setAccountInfo(accountResponse);
11289
+ console.log('[AuthContext] Proxy mode: initialized from parent account');
11290
+ notifyAuthStateChange('LOGIN', userFromAccount, null, accountResponse, accountResponse);
11291
+ }
11292
+ catch (error) {
11293
+ console.warn('[AuthContext] Proxy mode: failed to fetch account from parent:', error);
11294
+ // No session - that's ok, user may need to login
11295
+ }
11296
+ }
11297
+ else {
11298
+ console.log('[AuthContext] Proxy mode: no userId in URL, awaiting login');
11299
+ }
11300
+ setIsLoading(false);
11301
+ return;
11302
+ }
11303
+ // STANDALONE MODE: Load from persistent storage
11256
11304
  const storedToken = await tokenStorage.getToken();
11257
11305
  const storedUser = await tokenStorage.getUser();
11258
11306
  const storedAccountData = await tokenStorage.getAccountData();
@@ -11279,9 +11327,49 @@ const AuthProvider = ({ children, accountCacheTTL = 5 * 60 * 1000, preloadAccoun
11279
11327
  }
11280
11328
  };
11281
11329
  initializeAuth();
11282
- }, []);
11283
- // Cross-tab synchronization - listen for auth changes in other tabs
11330
+ }, [proxyMode, notifyAuthStateChange]);
11331
+ // Listen for parent auth state changes (proxy mode only)
11332
+ React.useEffect(() => {
11333
+ if (!proxyMode)
11334
+ return;
11335
+ console.log('[AuthContext] Proxy mode: setting up parent message listener');
11336
+ const handleParentMessage = (event) => {
11337
+ // Handle auth state pushed from parent
11338
+ if (event.data?.type === 'smartlinks:authkit:state') {
11339
+ const { user: parentUser, accountData: parentAccountData, authenticated } = event.data.payload || {};
11340
+ console.log('[AuthContext] Proxy mode: received state from parent:', { authenticated });
11341
+ if (authenticated && parentUser) {
11342
+ const userObj = {
11343
+ uid: parentUser.uid || parentUser.id,
11344
+ email: parentUser.email,
11345
+ displayName: parentUser.displayName || parentUser.name,
11346
+ phoneNumber: parentUser.phoneNumber,
11347
+ };
11348
+ setUser(userObj);
11349
+ setAccountData(parentAccountData || null);
11350
+ setAccountInfo(parentAccountData || null);
11351
+ notifyAuthStateChange('CROSS_TAB_SYNC', userObj, null, parentAccountData || null, parentAccountData || null);
11352
+ }
11353
+ else {
11354
+ // Parent indicates no session / logged out
11355
+ setUser(null);
11356
+ setToken(null);
11357
+ setAccountData(null);
11358
+ setAccountInfo(null);
11359
+ notifyAuthStateChange('LOGOUT', null, null, null, null);
11360
+ }
11361
+ }
11362
+ };
11363
+ window.addEventListener('message', handleParentMessage);
11364
+ return () => {
11365
+ console.log('[AuthContext] Proxy mode: cleaning up parent message listener');
11366
+ window.removeEventListener('message', handleParentMessage);
11367
+ };
11368
+ }, [proxyMode, notifyAuthStateChange]);
11369
+ // Cross-tab synchronization - standalone mode only
11284
11370
  React.useEffect(() => {
11371
+ if (proxyMode)
11372
+ return; // Skip cross-tab sync in proxy mode
11285
11373
  console.log('[AuthContext] Setting up cross-tab synchronization');
11286
11374
  const unsubscribe = onStorageChange(async (event) => {
11287
11375
  console.log('[AuthContext] Cross-tab storage event:', event.type, event.key);
@@ -11340,27 +11428,38 @@ const AuthProvider = ({ children, accountCacheTTL = 5 * 60 * 1000, preloadAccoun
11340
11428
  console.log('[AuthContext] Cleaning up cross-tab synchronization');
11341
11429
  unsubscribe();
11342
11430
  };
11343
- }, [notifyAuthStateChange]);
11431
+ }, [proxyMode, notifyAuthStateChange]);
11344
11432
  const login = React.useCallback(async (authToken, authUser, authAccountData) => {
11345
11433
  try {
11346
- // Store token, user, and account data
11347
- await tokenStorage.saveToken(authToken);
11348
- await tokenStorage.saveUser(authUser);
11349
- if (authAccountData) {
11350
- await tokenStorage.saveAccountData(authAccountData);
11434
+ // Only persist to storage in standalone mode
11435
+ if (!proxyMode) {
11436
+ await tokenStorage.saveToken(authToken);
11437
+ await tokenStorage.saveUser(authUser);
11438
+ if (authAccountData) {
11439
+ await tokenStorage.saveAccountData(authAccountData);
11440
+ }
11441
+ // Set bearer token in global Smartlinks SDK via auth.verifyToken
11442
+ smartlinks__namespace.auth.verifyToken(authToken).catch(err => {
11443
+ console.warn('Failed to set bearer token on login:', err);
11444
+ });
11351
11445
  }
11446
+ // Always update memory state
11352
11447
  setToken(authToken);
11353
11448
  setUser(authUser);
11354
11449
  setAccountData(authAccountData || null);
11355
- // Set bearer token in global Smartlinks SDK via auth.verifyToken
11356
- // This both validates the token and sets it for future API calls
11357
- smartlinks__namespace.auth.verifyToken(authToken).catch(err => {
11358
- console.warn('Failed to set bearer token on login:', err);
11359
- });
11450
+ // Cross-iframe auth state synchronization
11451
+ // Always notify parent frame of login (both modes, but especially important in proxy mode)
11452
+ const sdk = smartlinks__namespace;
11453
+ if (sdk.iframe?.isIframe?.()) {
11454
+ sdk.iframe.sendParentCustom('smartlinks:authkit:login', {
11455
+ token: authToken,
11456
+ user: authUser,
11457
+ accountData: authAccountData || null
11458
+ });
11459
+ }
11360
11460
  notifyAuthStateChange('LOGIN', authUser, authToken, authAccountData || null);
11361
- // Optionally preload account info on login
11362
- if (preloadAccountInfo) {
11363
- // Preload after login completes (non-blocking)
11461
+ // Optionally preload account info on login (standalone mode only)
11462
+ if (!proxyMode && preloadAccountInfo) {
11364
11463
  getAccount(true).catch(error => {
11365
11464
  console.warn('[AuthContext] Failed to preload account info:', error);
11366
11465
  });
@@ -11370,34 +11469,55 @@ const AuthProvider = ({ children, accountCacheTTL = 5 * 60 * 1000, preloadAccoun
11370
11469
  console.error('Failed to save auth data to storage:', error);
11371
11470
  throw error;
11372
11471
  }
11373
- }, [notifyAuthStateChange, preloadAccountInfo]);
11472
+ }, [proxyMode, notifyAuthStateChange, preloadAccountInfo]);
11374
11473
  const logout = React.useCallback(async () => {
11375
11474
  try {
11376
- // Clear persistent storage
11377
- await tokenStorage.clearAll();
11475
+ // Only clear persistent storage in standalone mode
11476
+ if (!proxyMode) {
11477
+ await tokenStorage.clearAll();
11478
+ smartlinks__namespace.auth.logout();
11479
+ }
11480
+ // Always clear memory state
11378
11481
  setToken(null);
11379
11482
  setUser(null);
11380
11483
  setAccountData(null);
11381
11484
  setAccountInfo(null);
11382
- // Clear bearer token from global Smartlinks SDK
11383
- smartlinks__namespace.auth.logout();
11485
+ // Cross-iframe auth state synchronization
11486
+ // Always notify parent frame of logout
11487
+ const sdk = smartlinks__namespace;
11488
+ if (sdk.iframe?.isIframe?.()) {
11489
+ sdk.iframe.sendParentCustom('smartlinks:authkit:logout', {});
11490
+ }
11384
11491
  notifyAuthStateChange('LOGOUT', null, null, null);
11385
11492
  }
11386
11493
  catch (error) {
11387
11494
  console.error('Failed to clear auth data from storage:', error);
11388
11495
  }
11389
- }, [notifyAuthStateChange]);
11496
+ }, [proxyMode, notifyAuthStateChange]);
11390
11497
  const getToken = React.useCallback(async () => {
11498
+ if (proxyMode) {
11499
+ // In proxy mode, token is managed by parent - return memory state
11500
+ return token;
11501
+ }
11391
11502
  const storedToken = await tokenStorage.getToken();
11392
11503
  return storedToken ? storedToken.token : null;
11393
- }, []);
11504
+ }, [proxyMode, token]);
11394
11505
  const refreshToken = React.useCallback(async () => {
11395
11506
  throw new Error('Token refresh must be implemented via your backend API');
11396
11507
  }, []);
11397
- // Get account with intelligent caching
11508
+ // Get account with intelligent caching (or direct parent fetch in proxy mode)
11398
11509
  const getAccount = React.useCallback(async (forceRefresh = false) => {
11399
11510
  try {
11400
- // Check if user is authenticated
11511
+ if (proxyMode) {
11512
+ // PROXY MODE: Always fetch from parent via proxied API, no local cache
11513
+ console.log('[AuthContext] Proxy mode: fetching account from parent');
11514
+ const freshAccountInfo = await smartlinks__namespace.auth.getAccount();
11515
+ setAccountInfo(freshAccountInfo);
11516
+ setAccountData(freshAccountInfo);
11517
+ notifyAuthStateChange('ACCOUNT_REFRESH', user, token, freshAccountInfo, freshAccountInfo);
11518
+ return freshAccountInfo;
11519
+ }
11520
+ // STANDALONE MODE: Use caching
11401
11521
  if (!token) {
11402
11522
  throw new Error('Not authenticated. Please login first.');
11403
11523
  }
@@ -11420,24 +11540,28 @@ const AuthProvider = ({ children, accountCacheTTL = 5 * 60 * 1000, preloadAccoun
11420
11540
  }
11421
11541
  catch (error) {
11422
11542
  console.error('[AuthContext] Failed to get account info:', error);
11423
- // Fallback to stale cache if API fails
11424
- const cached = await tokenStorage.getAccountInfo();
11425
- if (cached) {
11426
- console.warn('[AuthContext] Returning stale cached data due to API error');
11427
- return cached.data;
11543
+ // Fallback to stale cache if API fails (standalone mode only)
11544
+ if (!proxyMode) {
11545
+ const cached = await tokenStorage.getAccountInfo();
11546
+ if (cached) {
11547
+ console.warn('[AuthContext] Returning stale cached data due to API error');
11548
+ return cached.data;
11549
+ }
11428
11550
  }
11429
11551
  throw error;
11430
11552
  }
11431
- }, [token, accountCacheTTL, user, accountData, notifyAuthStateChange]);
11553
+ }, [proxyMode, token, accountCacheTTL, user, accountData, notifyAuthStateChange]);
11432
11554
  // Convenience method for explicit refresh
11433
11555
  const refreshAccount = React.useCallback(async () => {
11434
11556
  return await getAccount(true);
11435
11557
  }, [getAccount]);
11436
- // Clear account cache
11558
+ // Clear account cache (no-op in proxy mode)
11437
11559
  const clearAccountCache = React.useCallback(async () => {
11438
- await tokenStorage.clearAccountInfo();
11560
+ if (!proxyMode) {
11561
+ await tokenStorage.clearAccountInfo();
11562
+ }
11439
11563
  setAccountInfo(null);
11440
- }, []);
11564
+ }, [proxyMode]);
11441
11565
  const onAuthStateChange = React.useCallback((callback) => {
11442
11566
  callbacksRef.current.add(callback);
11443
11567
  // Return unsubscribe function
@@ -11450,8 +11574,9 @@ const AuthProvider = ({ children, accountCacheTTL = 5 * 60 * 1000, preloadAccoun
11450
11574
  token,
11451
11575
  accountData,
11452
11576
  accountInfo,
11453
- isAuthenticated: !!token && !!user,
11577
+ isAuthenticated: !!user,
11454
11578
  isLoading,
11579
+ proxyMode,
11455
11580
  login,
11456
11581
  logout,
11457
11582
  getToken,
@@ -11514,7 +11639,34 @@ const loadGoogleIdentityServices = () => {
11514
11639
  document.head.appendChild(script);
11515
11640
  });
11516
11641
  };
11517
- const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, enabledProviders = ['email', 'google', 'phone'], initialMode = 'login', redirectUrl, theme = 'auto', className, customization, skipConfigFetch = false, minimal = false, logger, }) => {
11642
+ // Helper to convert generic SDK errors to user-friendly messages
11643
+ const getFriendlyErrorMessage = (errorMessage) => {
11644
+ // Check for common HTTP status codes in the error message
11645
+ if (errorMessage.includes('status 400')) {
11646
+ return 'Invalid request. Please check your input and try again.';
11647
+ }
11648
+ if (errorMessage.includes('status 401')) {
11649
+ return 'Invalid credentials. Please check your email and password.';
11650
+ }
11651
+ if (errorMessage.includes('status 403')) {
11652
+ return 'Access denied. You do not have permission to perform this action.';
11653
+ }
11654
+ if (errorMessage.includes('status 404')) {
11655
+ return 'Account not found. Please check your email or create a new account.';
11656
+ }
11657
+ if (errorMessage.includes('status 409')) {
11658
+ return 'This email is already registered.';
11659
+ }
11660
+ if (errorMessage.includes('status 429')) {
11661
+ return 'Too many attempts. Please wait a moment and try again.';
11662
+ }
11663
+ if (errorMessage.includes('status 500') || errorMessage.includes('status 502') || errorMessage.includes('status 503')) {
11664
+ return 'Server error. Please try again later.';
11665
+ }
11666
+ // Return original message if no pattern matches
11667
+ return errorMessage;
11668
+ };
11669
+ 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, }) => {
11518
11670
  const [mode, setMode] = React.useState(initialMode);
11519
11671
  const [loading, setLoading] = React.useState(false);
11520
11672
  const [error, setError] = React.useState();
@@ -11547,25 +11699,25 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11547
11699
  mediaQuery.addEventListener('change', updateTheme);
11548
11700
  return () => mediaQuery.removeEventListener('change', updateTheme);
11549
11701
  }, [theme]);
11550
- // Reinitialize Smartlinks SDK when apiEndpoint changes (for test/dev scenarios)
11702
+ // Reinitialize Smartlinks SDK when apiEndpoint or proxyMode changes
11551
11703
  // IMPORTANT: Preserve bearer token during reinitialization
11552
11704
  React.useEffect(() => {
11553
- log.log('SDK reinitialize useEffect triggered', { apiEndpoint });
11705
+ log.log('SDK reinitialize useEffect triggered', { apiEndpoint, proxyMode });
11554
11706
  setSdkReady(false); // Mark SDK as not ready during reinitialization
11555
11707
  const reinitializeWithToken = async () => {
11556
11708
  if (apiEndpoint) {
11557
- log.log('Reinitializing SDK with baseURL:', apiEndpoint);
11558
- // Get current token before reinitializing
11559
- const currentToken = await auth.getToken();
11709
+ log.log('Reinitializing SDK with baseURL:', apiEndpoint, 'proxyMode:', proxyMode);
11710
+ // Get current token before reinitializing (only in standalone mode)
11711
+ const currentToken = !proxyMode ? await auth.getToken() : null;
11560
11712
  smartlinks__namespace.initializeApi({
11561
11713
  baseURL: apiEndpoint,
11562
- proxyMode: false, // Direct API calls when custom endpoint is provided
11714
+ proxyMode: proxyMode, // Use prop value
11563
11715
  ngrokSkipBrowserWarning: true,
11564
11716
  logger: logger, // Pass logger to SDK for verbose SDK logging
11565
11717
  });
11566
11718
  log.log('SDK reinitialized successfully');
11567
- // Restore bearer token after reinitialization using auth.verifyToken
11568
- if (currentToken) {
11719
+ // Restore bearer token after reinitialization using auth.verifyToken (standalone mode only)
11720
+ if (currentToken && !proxyMode) {
11569
11721
  smartlinks__namespace.auth.verifyToken(currentToken).catch(err => {
11570
11722
  log.warn('Failed to restore bearer token after reinit:', err);
11571
11723
  });
@@ -11573,6 +11725,11 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11573
11725
  // Mark SDK as ready
11574
11726
  setSdkReady(true);
11575
11727
  }
11728
+ else if (proxyMode) {
11729
+ // In proxy mode without custom endpoint, SDK should already be initialized by parent
11730
+ log.log('Proxy mode without apiEndpoint, SDK already initialized by parent');
11731
+ setSdkReady(true);
11732
+ }
11576
11733
  else {
11577
11734
  log.log('No apiEndpoint, SDK already initialized by App');
11578
11735
  // SDK was initialized by App component, mark as ready
@@ -11580,7 +11737,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11580
11737
  }
11581
11738
  };
11582
11739
  reinitializeWithToken();
11583
- }, [apiEndpoint, auth, logger, log]);
11740
+ }, [apiEndpoint, proxyMode, auth, logger, log]);
11584
11741
  // Get the effective redirect URL (use prop or default to current page)
11585
11742
  const getRedirectUrl = () => {
11586
11743
  if (redirectUrl)
@@ -11878,14 +12035,18 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11878
12035
  }
11879
12036
  catch (err) {
11880
12037
  const errorMessage = err instanceof Error ? err.message : 'Authentication failed';
11881
- // Check if error is about email already registered
11882
- if (mode === 'register' && errorMessage.toLowerCase().includes('already') && errorMessage.toLowerCase().includes('email')) {
12038
+ // Check if error is about email already registered (by content or 409 status code)
12039
+ const isAlreadyRegistered = mode === 'register' && ((errorMessage.toLowerCase().includes('already') && errorMessage.toLowerCase().includes('email')) ||
12040
+ errorMessage.includes('status 409'));
12041
+ if (isAlreadyRegistered) {
11883
12042
  setShowResendVerification(true);
11884
12043
  setResendEmail(data.email);
11885
12044
  setError('This email is already registered. If you didn\'t receive the verification email, you can resend it below.');
11886
12045
  }
11887
12046
  else {
11888
- setError(errorMessage);
12047
+ // Try to extract a more meaningful error message from status codes
12048
+ const friendlyMessage = getFriendlyErrorMessage(errorMessage);
12049
+ setError(friendlyMessage);
11889
12050
  }
11890
12051
  onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11891
12052
  }
@@ -11941,6 +12102,15 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11941
12102
  const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
11942
12103
  // Determine OAuth flow: default to 'oneTap' for better UX, but allow 'popup' for iframe compatibility
11943
12104
  const oauthFlow = config?.googleOAuthFlow || 'oneTap';
12105
+ // Log Google Auth configuration for debugging
12106
+ log.log('Google Auth initiated:', {
12107
+ googleClientId,
12108
+ oauthFlow,
12109
+ currentOrigin: window.location.origin,
12110
+ currentHref: window.location.href,
12111
+ configGoogleClientId: config?.googleClientId,
12112
+ usingDefaultClientId: !config?.googleClientId,
12113
+ });
11944
12114
  setLoading(true);
11945
12115
  setError(undefined);
11946
12116
  try {
@@ -11950,35 +12120,87 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11950
12120
  if (!google?.accounts) {
11951
12121
  throw new Error('Google Identity Services failed to initialize');
11952
12122
  }
12123
+ log.log('Google Identity Services loaded, using flow:', oauthFlow);
11953
12124
  if (oauthFlow === 'popup') {
11954
12125
  // Use OAuth2 popup flow (works in iframes but requires popup permission)
11955
12126
  if (!google.accounts.oauth2) {
11956
12127
  throw new Error('Google OAuth2 not available');
11957
12128
  }
12129
+ log.log('Initializing Google OAuth2 popup flow:', {
12130
+ client_id: googleClientId,
12131
+ scope: 'openid email profile',
12132
+ origin: window.location.origin,
12133
+ });
11958
12134
  const client = google.accounts.oauth2.initTokenClient({
11959
12135
  client_id: googleClientId,
11960
12136
  scope: 'openid email profile',
11961
12137
  callback: async (response) => {
11962
12138
  try {
12139
+ log.log('Google OAuth2 popup callback received:', {
12140
+ hasAccessToken: !!response.access_token,
12141
+ hasIdToken: !!response.id_token,
12142
+ tokenType: response.token_type,
12143
+ expiresIn: response.expires_in,
12144
+ scope: response.scope,
12145
+ error: response.error,
12146
+ errorDescription: response.error_description,
12147
+ });
11963
12148
  if (response.error) {
11964
12149
  throw new Error(response.error_description || response.error);
11965
12150
  }
12151
+ // OAuth2 popup flow returns access_token, not id_token
12152
+ // We need to use the access token to get user info from Google
11966
12153
  const accessToken = response.access_token;
11967
- // Send access token to backend
11968
- const authResponse = await api.loginWithGoogle(accessToken);
11969
- if (authResponse.token) {
11970
- auth.login(authResponse.token, authResponse.user, authResponse.accountData);
11971
- setAuthSuccess(true);
11972
- setSuccessMessage('Google login successful!');
11973
- onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
12154
+ if (!accessToken) {
12155
+ throw new Error('No access token received from Google');
11974
12156
  }
11975
- else {
11976
- throw new Error('Authentication failed - no token received');
12157
+ log.log('Fetching user info from Google using access token...');
12158
+ // Fetch user info from Google's userinfo endpoint
12159
+ const userInfoResponse = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
12160
+ headers: {
12161
+ 'Authorization': `Bearer ${accessToken}`,
12162
+ },
12163
+ });
12164
+ if (!userInfoResponse.ok) {
12165
+ throw new Error('Failed to fetch user info from Google');
11977
12166
  }
11978
- if (redirectUrl) {
11979
- setTimeout(() => {
11980
- window.location.href = redirectUrl;
11981
- }, 2000);
12167
+ const userInfo = await userInfoResponse.json();
12168
+ log.log('Google user info retrieved:', {
12169
+ email: userInfo.email,
12170
+ name: userInfo.name,
12171
+ sub: userInfo.sub,
12172
+ });
12173
+ // For popup flow, send the access token to backend
12174
+ // Note: This may fail if backend only supports ID token verification
12175
+ try {
12176
+ const authResponse = await api.loginWithGoogle(accessToken, {
12177
+ googleUserInfo: userInfo,
12178
+ tokenType: 'access_token',
12179
+ });
12180
+ if (authResponse.token) {
12181
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData);
12182
+ setAuthSuccess(true);
12183
+ setSuccessMessage('Google login successful!');
12184
+ onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
12185
+ }
12186
+ else {
12187
+ throw new Error('Authentication failed - no token received');
12188
+ }
12189
+ if (redirectUrl) {
12190
+ setTimeout(() => {
12191
+ window.location.href = redirectUrl;
12192
+ }, 2000);
12193
+ }
12194
+ }
12195
+ catch (apiError) {
12196
+ const errorMessage = apiError instanceof Error ? apiError.message : 'Google login failed';
12197
+ // Check if this is the access token vs ID token mismatch
12198
+ if (errorMessage.includes('Invalid or expired Google token')) {
12199
+ log.error('Popup flow access token rejected by backend. Backend may only support ID tokens.');
12200
+ log.error('User info retrieved from Google:', userInfo);
12201
+ throw new Error('Google authentication failed. The popup flow may not be supported. Please try again or contact support.');
12202
+ }
12203
+ throw apiError;
11982
12204
  }
11983
12205
  setLoading(false);
11984
12206
  }
@@ -11994,6 +12216,10 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11994
12216
  }
11995
12217
  else {
11996
12218
  // Use One Tap / Sign-In button flow (smoother UX but doesn't work in iframes)
12219
+ log.log('Initializing Google OneTap flow:', {
12220
+ client_id: googleClientId,
12221
+ origin: window.location.origin,
12222
+ });
11997
12223
  google.accounts.id.initialize({
11998
12224
  client_id: googleClientId,
11999
12225
  callback: async (response) => {
@@ -12025,11 +12251,18 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12025
12251
  },
12026
12252
  auto_select: false,
12027
12253
  cancel_on_tap_outside: true,
12254
+ // Note: use_fedcm_for_prompt omitted - requires Permissions-Policy header on hosting server
12255
+ // Will be needed when FedCM becomes mandatory in the future
12028
12256
  });
12029
- google.accounts.id.prompt((notification) => {
12030
- if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
12031
- setLoading(false);
12032
- }
12257
+ // Use timeout fallback instead of deprecated notification methods
12258
+ // (isNotDisplayed/isSkippedMoment will stop working when FedCM becomes mandatory)
12259
+ const promptTimeout = setTimeout(() => {
12260
+ setLoading(false);
12261
+ }, 5000);
12262
+ google.accounts.id.prompt(() => {
12263
+ // Clear timeout if prompt interaction occurs
12264
+ clearTimeout(promptTimeout);
12265
+ setLoading(false);
12033
12266
  });
12034
12267
  }
12035
12268
  }