@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.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);
@@ -11514,6 +11527,33 @@ const loadGoogleIdentityServices = () => {
11514
11527
  document.head.appendChild(script);
11515
11528
  });
11516
11529
  };
11530
+ // Helper to convert generic SDK errors to user-friendly messages
11531
+ const getFriendlyErrorMessage = (errorMessage) => {
11532
+ // Check for common HTTP status codes in the error message
11533
+ if (errorMessage.includes('status 400')) {
11534
+ return 'Invalid request. Please check your input and try again.';
11535
+ }
11536
+ if (errorMessage.includes('status 401')) {
11537
+ return 'Invalid credentials. Please check your email and password.';
11538
+ }
11539
+ if (errorMessage.includes('status 403')) {
11540
+ return 'Access denied. You do not have permission to perform this action.';
11541
+ }
11542
+ if (errorMessage.includes('status 404')) {
11543
+ return 'Account not found. Please check your email or create a new account.';
11544
+ }
11545
+ if (errorMessage.includes('status 409')) {
11546
+ return 'This email is already registered.';
11547
+ }
11548
+ if (errorMessage.includes('status 429')) {
11549
+ return 'Too many attempts. Please wait a moment and try again.';
11550
+ }
11551
+ if (errorMessage.includes('status 500') || errorMessage.includes('status 502') || errorMessage.includes('status 503')) {
11552
+ return 'Server error. Please try again later.';
11553
+ }
11554
+ // Return original message if no pattern matches
11555
+ return errorMessage;
11556
+ };
11517
11557
  const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, enabledProviders = ['email', 'google', 'phone'], initialMode = 'login', redirectUrl, theme = 'auto', className, customization, skipConfigFetch = false, minimal = false, logger, }) => {
11518
11558
  const [mode, setMode] = React.useState(initialMode);
11519
11559
  const [loading, setLoading] = React.useState(false);
@@ -11530,6 +11570,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11530
11570
  const [config, setConfig] = React.useState(null);
11531
11571
  const [configLoading, setConfigLoading] = React.useState(!skipConfigFetch);
11532
11572
  const [showEmailForm, setShowEmailForm] = React.useState(false); // Track if email form should be shown when emailDisplayMode is 'button'
11573
+ const [sdkReady, setSdkReady] = React.useState(false); // Track SDK initialization state
11533
11574
  const log = React.useMemo(() => createLoggerWrapper(logger), [logger]);
11534
11575
  const api = new AuthAPI(apiEndpoint, clientId, clientName, logger);
11535
11576
  const auth = useAuth();
@@ -11550,6 +11591,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11550
11591
  // IMPORTANT: Preserve bearer token during reinitialization
11551
11592
  React.useEffect(() => {
11552
11593
  log.log('SDK reinitialize useEffect triggered', { apiEndpoint });
11594
+ setSdkReady(false); // Mark SDK as not ready during reinitialization
11553
11595
  const reinitializeWithToken = async () => {
11554
11596
  if (apiEndpoint) {
11555
11597
  log.log('Reinitializing SDK with baseURL:', apiEndpoint);
@@ -11561,15 +11603,20 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11561
11603
  ngrokSkipBrowserWarning: true,
11562
11604
  logger: logger, // Pass logger to SDK for verbose SDK logging
11563
11605
  });
11606
+ log.log('SDK reinitialized successfully');
11564
11607
  // Restore bearer token after reinitialization using auth.verifyToken
11565
11608
  if (currentToken) {
11566
11609
  smartlinks__namespace.auth.verifyToken(currentToken).catch(err => {
11567
11610
  log.warn('Failed to restore bearer token after reinit:', err);
11568
11611
  });
11569
11612
  }
11613
+ // Mark SDK as ready
11614
+ setSdkReady(true);
11570
11615
  }
11571
11616
  else {
11572
- log.log('No apiEndpoint, skipping SDK reinitialize');
11617
+ log.log('No apiEndpoint, SDK already initialized by App');
11618
+ // SDK was initialized by App component, mark as ready
11619
+ setSdkReady(true);
11573
11620
  }
11574
11621
  };
11575
11622
  reinitializeWithToken();
@@ -11590,8 +11637,14 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11590
11637
  clientId,
11591
11638
  clientIdType: typeof clientId,
11592
11639
  clientIdTruthy: !!clientId,
11593
- apiEndpoint
11640
+ apiEndpoint,
11641
+ sdkReady
11594
11642
  });
11643
+ // Wait for SDK to be ready before fetching config
11644
+ if (!sdkReady) {
11645
+ log.log('SDK not ready yet, waiting...');
11646
+ return;
11647
+ }
11595
11648
  if (skipConfigFetch) {
11596
11649
  log.log('Skipping config fetch - skipConfigFetch is true');
11597
11650
  setConfig(customization || {});
@@ -11652,7 +11705,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11652
11705
  }
11653
11706
  };
11654
11707
  fetchConfig();
11655
- }, [apiEndpoint, clientId, customization, skipConfigFetch, log]);
11708
+ }, [apiEndpoint, clientId, customization, skipConfigFetch, sdkReady, log]);
11656
11709
  // Reset showEmailForm when mode changes away from login/register
11657
11710
  React.useEffect(() => {
11658
11711
  if (mode !== 'login' && mode !== 'register') {
@@ -11865,14 +11918,18 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11865
11918
  }
11866
11919
  catch (err) {
11867
11920
  const errorMessage = err instanceof Error ? err.message : 'Authentication failed';
11868
- // Check if error is about email already registered
11869
- if (mode === 'register' && errorMessage.toLowerCase().includes('already') && errorMessage.toLowerCase().includes('email')) {
11921
+ // Check if error is about email already registered (by content or 409 status code)
11922
+ const isAlreadyRegistered = mode === 'register' && ((errorMessage.toLowerCase().includes('already') && errorMessage.toLowerCase().includes('email')) ||
11923
+ errorMessage.includes('status 409'));
11924
+ if (isAlreadyRegistered) {
11870
11925
  setShowResendVerification(true);
11871
11926
  setResendEmail(data.email);
11872
11927
  setError('This email is already registered. If you didn\'t receive the verification email, you can resend it below.');
11873
11928
  }
11874
11929
  else {
11875
- setError(errorMessage);
11930
+ // Try to extract a more meaningful error message from status codes
11931
+ const friendlyMessage = getFriendlyErrorMessage(errorMessage);
11932
+ setError(friendlyMessage);
11876
11933
  }
11877
11934
  onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11878
11935
  }
@@ -11928,6 +11985,15 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11928
11985
  const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
11929
11986
  // Determine OAuth flow: default to 'oneTap' for better UX, but allow 'popup' for iframe compatibility
11930
11987
  const oauthFlow = config?.googleOAuthFlow || 'oneTap';
11988
+ // Log Google Auth configuration for debugging
11989
+ log.log('Google Auth initiated:', {
11990
+ googleClientId,
11991
+ oauthFlow,
11992
+ currentOrigin: window.location.origin,
11993
+ currentHref: window.location.href,
11994
+ configGoogleClientId: config?.googleClientId,
11995
+ usingDefaultClientId: !config?.googleClientId,
11996
+ });
11931
11997
  setLoading(true);
11932
11998
  setError(undefined);
11933
11999
  try {
@@ -11937,35 +12003,87 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11937
12003
  if (!google?.accounts) {
11938
12004
  throw new Error('Google Identity Services failed to initialize');
11939
12005
  }
12006
+ log.log('Google Identity Services loaded, using flow:', oauthFlow);
11940
12007
  if (oauthFlow === 'popup') {
11941
12008
  // Use OAuth2 popup flow (works in iframes but requires popup permission)
11942
12009
  if (!google.accounts.oauth2) {
11943
12010
  throw new Error('Google OAuth2 not available');
11944
12011
  }
12012
+ log.log('Initializing Google OAuth2 popup flow:', {
12013
+ client_id: googleClientId,
12014
+ scope: 'openid email profile',
12015
+ origin: window.location.origin,
12016
+ });
11945
12017
  const client = google.accounts.oauth2.initTokenClient({
11946
12018
  client_id: googleClientId,
11947
12019
  scope: 'openid email profile',
11948
12020
  callback: async (response) => {
11949
12021
  try {
12022
+ log.log('Google OAuth2 popup callback received:', {
12023
+ hasAccessToken: !!response.access_token,
12024
+ hasIdToken: !!response.id_token,
12025
+ tokenType: response.token_type,
12026
+ expiresIn: response.expires_in,
12027
+ scope: response.scope,
12028
+ error: response.error,
12029
+ errorDescription: response.error_description,
12030
+ });
11950
12031
  if (response.error) {
11951
12032
  throw new Error(response.error_description || response.error);
11952
12033
  }
12034
+ // OAuth2 popup flow returns access_token, not id_token
12035
+ // We need to use the access token to get user info from Google
11953
12036
  const accessToken = response.access_token;
11954
- // Send access token to backend
11955
- const authResponse = await api.loginWithGoogle(accessToken);
11956
- if (authResponse.token) {
11957
- auth.login(authResponse.token, authResponse.user, authResponse.accountData);
11958
- setAuthSuccess(true);
11959
- setSuccessMessage('Google login successful!');
11960
- onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
12037
+ if (!accessToken) {
12038
+ throw new Error('No access token received from Google');
11961
12039
  }
11962
- else {
11963
- throw new Error('Authentication failed - no token received');
12040
+ log.log('Fetching user info from Google using access token...');
12041
+ // Fetch user info from Google's userinfo endpoint
12042
+ const userInfoResponse = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
12043
+ headers: {
12044
+ 'Authorization': `Bearer ${accessToken}`,
12045
+ },
12046
+ });
12047
+ if (!userInfoResponse.ok) {
12048
+ throw new Error('Failed to fetch user info from Google');
11964
12049
  }
11965
- if (redirectUrl) {
11966
- setTimeout(() => {
11967
- window.location.href = redirectUrl;
11968
- }, 2000);
12050
+ const userInfo = await userInfoResponse.json();
12051
+ log.log('Google user info retrieved:', {
12052
+ email: userInfo.email,
12053
+ name: userInfo.name,
12054
+ sub: userInfo.sub,
12055
+ });
12056
+ // For popup flow, send the access token to backend
12057
+ // Note: This may fail if backend only supports ID token verification
12058
+ try {
12059
+ const authResponse = await api.loginWithGoogle(accessToken, {
12060
+ googleUserInfo: userInfo,
12061
+ tokenType: 'access_token',
12062
+ });
12063
+ if (authResponse.token) {
12064
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData);
12065
+ setAuthSuccess(true);
12066
+ setSuccessMessage('Google login successful!');
12067
+ onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
12068
+ }
12069
+ else {
12070
+ throw new Error('Authentication failed - no token received');
12071
+ }
12072
+ if (redirectUrl) {
12073
+ setTimeout(() => {
12074
+ window.location.href = redirectUrl;
12075
+ }, 2000);
12076
+ }
12077
+ }
12078
+ catch (apiError) {
12079
+ const errorMessage = apiError instanceof Error ? apiError.message : 'Google login failed';
12080
+ // Check if this is the access token vs ID token mismatch
12081
+ if (errorMessage.includes('Invalid or expired Google token')) {
12082
+ log.error('Popup flow access token rejected by backend. Backend may only support ID tokens.');
12083
+ log.error('User info retrieved from Google:', userInfo);
12084
+ throw new Error('Google authentication failed. The popup flow may not be supported. Please try again or contact support.');
12085
+ }
12086
+ throw apiError;
11969
12087
  }
11970
12088
  setLoading(false);
11971
12089
  }
@@ -11981,6 +12099,10 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11981
12099
  }
11982
12100
  else {
11983
12101
  // Use One Tap / Sign-In button flow (smoother UX but doesn't work in iframes)
12102
+ log.log('Initializing Google OneTap flow:', {
12103
+ client_id: googleClientId,
12104
+ origin: window.location.origin,
12105
+ });
11984
12106
  google.accounts.id.initialize({
11985
12107
  client_id: googleClientId,
11986
12108
  callback: async (response) => {