@proveanything/smartlinks-auth-ui 0.1.9 → 0.1.11

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);
@@ -11493,6 +11506,33 @@ const loadGoogleIdentityServices = () => {
11493
11506
  document.head.appendChild(script);
11494
11507
  });
11495
11508
  };
11509
+ // Helper to convert generic SDK errors to user-friendly messages
11510
+ const getFriendlyErrorMessage = (errorMessage) => {
11511
+ // Check for common HTTP status codes in the error message
11512
+ if (errorMessage.includes('status 400')) {
11513
+ return 'Invalid request. Please check your input and try again.';
11514
+ }
11515
+ if (errorMessage.includes('status 401')) {
11516
+ return 'Invalid credentials. Please check your email and password.';
11517
+ }
11518
+ if (errorMessage.includes('status 403')) {
11519
+ return 'Access denied. You do not have permission to perform this action.';
11520
+ }
11521
+ if (errorMessage.includes('status 404')) {
11522
+ return 'Account not found. Please check your email or create a new account.';
11523
+ }
11524
+ if (errorMessage.includes('status 409')) {
11525
+ return 'This email is already registered.';
11526
+ }
11527
+ if (errorMessage.includes('status 429')) {
11528
+ return 'Too many attempts. Please wait a moment and try again.';
11529
+ }
11530
+ if (errorMessage.includes('status 500') || errorMessage.includes('status 502') || errorMessage.includes('status 503')) {
11531
+ return 'Server error. Please try again later.';
11532
+ }
11533
+ // Return original message if no pattern matches
11534
+ return errorMessage;
11535
+ };
11496
11536
  const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, enabledProviders = ['email', 'google', 'phone'], initialMode = 'login', redirectUrl, theme = 'auto', className, customization, skipConfigFetch = false, minimal = false, logger, }) => {
11497
11537
  const [mode, setMode] = useState(initialMode);
11498
11538
  const [loading, setLoading] = useState(false);
@@ -11509,6 +11549,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11509
11549
  const [config, setConfig] = useState(null);
11510
11550
  const [configLoading, setConfigLoading] = useState(!skipConfigFetch);
11511
11551
  const [showEmailForm, setShowEmailForm] = useState(false); // Track if email form should be shown when emailDisplayMode is 'button'
11552
+ const [sdkReady, setSdkReady] = useState(false); // Track SDK initialization state
11512
11553
  const log = useMemo(() => createLoggerWrapper(logger), [logger]);
11513
11554
  const api = new AuthAPI(apiEndpoint, clientId, clientName, logger);
11514
11555
  const auth = useAuth();
@@ -11529,6 +11570,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11529
11570
  // IMPORTANT: Preserve bearer token during reinitialization
11530
11571
  useEffect(() => {
11531
11572
  log.log('SDK reinitialize useEffect triggered', { apiEndpoint });
11573
+ setSdkReady(false); // Mark SDK as not ready during reinitialization
11532
11574
  const reinitializeWithToken = async () => {
11533
11575
  if (apiEndpoint) {
11534
11576
  log.log('Reinitializing SDK with baseURL:', apiEndpoint);
@@ -11540,15 +11582,20 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11540
11582
  ngrokSkipBrowserWarning: true,
11541
11583
  logger: logger, // Pass logger to SDK for verbose SDK logging
11542
11584
  });
11585
+ log.log('SDK reinitialized successfully');
11543
11586
  // Restore bearer token after reinitialization using auth.verifyToken
11544
11587
  if (currentToken) {
11545
11588
  smartlinks.auth.verifyToken(currentToken).catch(err => {
11546
11589
  log.warn('Failed to restore bearer token after reinit:', err);
11547
11590
  });
11548
11591
  }
11592
+ // Mark SDK as ready
11593
+ setSdkReady(true);
11549
11594
  }
11550
11595
  else {
11551
- log.log('No apiEndpoint, skipping SDK reinitialize');
11596
+ log.log('No apiEndpoint, SDK already initialized by App');
11597
+ // SDK was initialized by App component, mark as ready
11598
+ setSdkReady(true);
11552
11599
  }
11553
11600
  };
11554
11601
  reinitializeWithToken();
@@ -11569,8 +11616,14 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11569
11616
  clientId,
11570
11617
  clientIdType: typeof clientId,
11571
11618
  clientIdTruthy: !!clientId,
11572
- apiEndpoint
11619
+ apiEndpoint,
11620
+ sdkReady
11573
11621
  });
11622
+ // Wait for SDK to be ready before fetching config
11623
+ if (!sdkReady) {
11624
+ log.log('SDK not ready yet, waiting...');
11625
+ return;
11626
+ }
11574
11627
  if (skipConfigFetch) {
11575
11628
  log.log('Skipping config fetch - skipConfigFetch is true');
11576
11629
  setConfig(customization || {});
@@ -11631,7 +11684,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11631
11684
  }
11632
11685
  };
11633
11686
  fetchConfig();
11634
- }, [apiEndpoint, clientId, customization, skipConfigFetch, log]);
11687
+ }, [apiEndpoint, clientId, customization, skipConfigFetch, sdkReady, log]);
11635
11688
  // Reset showEmailForm when mode changes away from login/register
11636
11689
  useEffect(() => {
11637
11690
  if (mode !== 'login' && mode !== 'register') {
@@ -11844,14 +11897,18 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11844
11897
  }
11845
11898
  catch (err) {
11846
11899
  const errorMessage = err instanceof Error ? err.message : 'Authentication failed';
11847
- // Check if error is about email already registered
11848
- if (mode === 'register' && errorMessage.toLowerCase().includes('already') && errorMessage.toLowerCase().includes('email')) {
11900
+ // Check if error is about email already registered (by content or 409 status code)
11901
+ const isAlreadyRegistered = mode === 'register' && ((errorMessage.toLowerCase().includes('already') && errorMessage.toLowerCase().includes('email')) ||
11902
+ errorMessage.includes('status 409'));
11903
+ if (isAlreadyRegistered) {
11849
11904
  setShowResendVerification(true);
11850
11905
  setResendEmail(data.email);
11851
11906
  setError('This email is already registered. If you didn\'t receive the verification email, you can resend it below.');
11852
11907
  }
11853
11908
  else {
11854
- setError(errorMessage);
11909
+ // Try to extract a more meaningful error message from status codes
11910
+ const friendlyMessage = getFriendlyErrorMessage(errorMessage);
11911
+ setError(friendlyMessage);
11855
11912
  }
11856
11913
  onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11857
11914
  }
@@ -11907,6 +11964,15 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11907
11964
  const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
11908
11965
  // Determine OAuth flow: default to 'oneTap' for better UX, but allow 'popup' for iframe compatibility
11909
11966
  const oauthFlow = config?.googleOAuthFlow || 'oneTap';
11967
+ // Log Google Auth configuration for debugging
11968
+ log.log('Google Auth initiated:', {
11969
+ googleClientId,
11970
+ oauthFlow,
11971
+ currentOrigin: window.location.origin,
11972
+ currentHref: window.location.href,
11973
+ configGoogleClientId: config?.googleClientId,
11974
+ usingDefaultClientId: !config?.googleClientId,
11975
+ });
11910
11976
  setLoading(true);
11911
11977
  setError(undefined);
11912
11978
  try {
@@ -11916,35 +11982,87 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11916
11982
  if (!google?.accounts) {
11917
11983
  throw new Error('Google Identity Services failed to initialize');
11918
11984
  }
11985
+ log.log('Google Identity Services loaded, using flow:', oauthFlow);
11919
11986
  if (oauthFlow === 'popup') {
11920
11987
  // Use OAuth2 popup flow (works in iframes but requires popup permission)
11921
11988
  if (!google.accounts.oauth2) {
11922
11989
  throw new Error('Google OAuth2 not available');
11923
11990
  }
11991
+ log.log('Initializing Google OAuth2 popup flow:', {
11992
+ client_id: googleClientId,
11993
+ scope: 'openid email profile',
11994
+ origin: window.location.origin,
11995
+ });
11924
11996
  const client = google.accounts.oauth2.initTokenClient({
11925
11997
  client_id: googleClientId,
11926
11998
  scope: 'openid email profile',
11927
11999
  callback: async (response) => {
11928
12000
  try {
12001
+ log.log('Google OAuth2 popup callback received:', {
12002
+ hasAccessToken: !!response.access_token,
12003
+ hasIdToken: !!response.id_token,
12004
+ tokenType: response.token_type,
12005
+ expiresIn: response.expires_in,
12006
+ scope: response.scope,
12007
+ error: response.error,
12008
+ errorDescription: response.error_description,
12009
+ });
11929
12010
  if (response.error) {
11930
12011
  throw new Error(response.error_description || response.error);
11931
12012
  }
12013
+ // OAuth2 popup flow returns access_token, not id_token
12014
+ // We need to use the access token to get user info from Google
11932
12015
  const accessToken = response.access_token;
11933
- // Send access token to backend
11934
- const authResponse = await api.loginWithGoogle(accessToken);
11935
- if (authResponse.token) {
11936
- auth.login(authResponse.token, authResponse.user, authResponse.accountData);
11937
- setAuthSuccess(true);
11938
- setSuccessMessage('Google login successful!');
11939
- onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
12016
+ if (!accessToken) {
12017
+ throw new Error('No access token received from Google');
11940
12018
  }
11941
- else {
11942
- throw new Error('Authentication failed - no token received');
12019
+ log.log('Fetching user info from Google using access token...');
12020
+ // Fetch user info from Google's userinfo endpoint
12021
+ const userInfoResponse = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
12022
+ headers: {
12023
+ 'Authorization': `Bearer ${accessToken}`,
12024
+ },
12025
+ });
12026
+ if (!userInfoResponse.ok) {
12027
+ throw new Error('Failed to fetch user info from Google');
11943
12028
  }
11944
- if (redirectUrl) {
11945
- setTimeout(() => {
11946
- window.location.href = redirectUrl;
11947
- }, 2000);
12029
+ const userInfo = await userInfoResponse.json();
12030
+ log.log('Google user info retrieved:', {
12031
+ email: userInfo.email,
12032
+ name: userInfo.name,
12033
+ sub: userInfo.sub,
12034
+ });
12035
+ // For popup flow, send the access token to backend
12036
+ // Note: This may fail if backend only supports ID token verification
12037
+ try {
12038
+ const authResponse = await api.loginWithGoogle(accessToken, {
12039
+ googleUserInfo: userInfo,
12040
+ tokenType: 'access_token',
12041
+ });
12042
+ if (authResponse.token) {
12043
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData);
12044
+ setAuthSuccess(true);
12045
+ setSuccessMessage('Google login successful!');
12046
+ onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
12047
+ }
12048
+ else {
12049
+ throw new Error('Authentication failed - no token received');
12050
+ }
12051
+ if (redirectUrl) {
12052
+ setTimeout(() => {
12053
+ window.location.href = redirectUrl;
12054
+ }, 2000);
12055
+ }
12056
+ }
12057
+ catch (apiError) {
12058
+ const errorMessage = apiError instanceof Error ? apiError.message : 'Google login failed';
12059
+ // Check if this is the access token vs ID token mismatch
12060
+ if (errorMessage.includes('Invalid or expired Google token')) {
12061
+ log.error('Popup flow access token rejected by backend. Backend may only support ID tokens.');
12062
+ log.error('User info retrieved from Google:', userInfo);
12063
+ throw new Error('Google authentication failed. The popup flow may not be supported. Please try again or contact support.');
12064
+ }
12065
+ throw apiError;
11948
12066
  }
11949
12067
  setLoading(false);
11950
12068
  }
@@ -11960,6 +12078,10 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11960
12078
  }
11961
12079
  else {
11962
12080
  // Use One Tap / Sign-In button flow (smoother UX but doesn't work in iframes)
12081
+ log.log('Initializing Google OneTap flow:', {
12082
+ client_id: googleClientId,
12083
+ origin: window.location.origin,
12084
+ });
11963
12085
  google.accounts.id.initialize({
11964
12086
  client_id: googleClientId,
11965
12087
  callback: async (response) => {