@proveanything/smartlinks-auth-ui 0.4.1 → 0.4.4

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
@@ -1,8 +1,7 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import React, { useEffect, useState, useMemo, useRef, useCallback, createContext, useContext } from 'react';
3
3
  import * as smartlinks from '@proveanything/smartlinks';
4
- import { evaluateConditions, iframe, SmartlinksApiError } from '@proveanything/smartlinks';
5
- export { evaluateConditions } from '@proveanything/smartlinks';
4
+ import { iframe, SmartlinksApiError } from '@proveanything/smartlinks';
6
5
  import { post, getApiHeaders, isProxyEnabled } from '@proveanything/smartlinks/dist/http';
7
6
 
8
7
  const AuthContainer = ({ children, theme = 'light', className = '', config, minimal = false, }) => {
@@ -60,6 +59,52 @@ const AuthContainer = ({ children, theme = 'light', className = '', config, mini
60
59
  return (jsx("div", { className: containerClass, children: jsxs("div", { className: cardClass, style: !minimal && config?.branding?.buttonStyle === 'square' ? { borderRadius: '4px' } : undefined, children: [(logoUrl || title || subtitle) && (jsxs("div", { className: "auth-header", children: [logoUrl && (jsx("div", { className: "auth-logo", children: jsx("img", { src: logoUrl, alt: "Logo", style: { maxWidth: '200px', height: 'auto', objectFit: 'contain' } }) })), title && jsx("h1", { className: "auth-title", children: title }), subtitle && jsx("p", { className: "auth-subtitle", children: subtitle })] })), jsx("div", { className: "auth-content", children: children }), (config?.branding?.termsUrl || config?.branding?.privacyUrl) && (jsxs("div", { className: "auth-footer", children: [config.branding.termsUrl && jsx("a", { href: config.branding.termsUrl, target: "_blank", rel: "noopener noreferrer", children: "Terms" }), config.branding.termsUrl && config.branding.privacyUrl && jsx("span", { children: "\u2022" }), config.branding.privacyUrl && jsx("a", { href: config.branding.privacyUrl, target: "_blank", rel: "noopener noreferrer", children: "Privacy" })] }))] }) }));
61
60
  };
62
61
 
62
+ /**
63
+ * Evaluate whether a field's conditions are satisfied given current form values.
64
+ * Local implementation matching SDK's evaluateConditions — avoids Vite ESM export issues.
65
+ */
66
+ const evaluateConditions = (conditions, showWhen, fieldValues) => {
67
+ if (!conditions || conditions.length === 0)
68
+ return true;
69
+ const results = conditions.map(condition => {
70
+ const value = fieldValues[condition.targetFieldId];
71
+ switch (condition.operator) {
72
+ case 'is_empty':
73
+ return value == null || value === '' || (Array.isArray(value) && value.length === 0);
74
+ case 'is_not_empty':
75
+ return value != null && value !== '' && !(Array.isArray(value) && value.length === 0);
76
+ case 'is_true':
77
+ return value === true;
78
+ case 'is_false':
79
+ return value === false;
80
+ case 'equals':
81
+ return value === condition.value;
82
+ case 'not_equals':
83
+ return value !== condition.value;
84
+ case 'contains':
85
+ return Array.isArray(value)
86
+ ? value.includes(condition.value)
87
+ : typeof value === 'string' && value.includes(String(condition.value));
88
+ case 'not_contains':
89
+ return Array.isArray(value)
90
+ ? !value.includes(condition.value)
91
+ : typeof value === 'string' && !value.includes(String(condition.value));
92
+ case 'greater_than':
93
+ return typeof value === 'number' && typeof condition.value === 'number'
94
+ ? value > condition.value
95
+ : String(value) > String(condition.value);
96
+ case 'less_than':
97
+ return typeof value === 'number' && typeof condition.value === 'number'
98
+ ? value < condition.value
99
+ : String(value) < String(condition.value);
100
+ default:
101
+ return true;
102
+ }
103
+ });
104
+ return (showWhen ?? 'all') === 'any'
105
+ ? results.some(Boolean)
106
+ : results.every(Boolean);
107
+ };
63
108
  /**
64
109
  * Renders a form field based on a ContactSchemaResponse property + uiSchema entry.
65
110
  */
@@ -130,10 +175,13 @@ const SchemaFieldRenderer = ({ field, value, onChange, disabled = false, error,
130
175
  const resolveFields = (schema, formValues) => {
131
176
  if (!schema)
132
177
  return [];
133
- const requiredSet = new Set(schema.schema.required || []);
134
- return schema.fieldOrder
178
+ const fieldOrder = schema.fieldOrder ?? [];
179
+ const properties = schema.schema?.properties ?? {};
180
+ const uiSchema = schema.uiSchema ?? {};
181
+ const requiredSet = new Set(schema.schema?.required || []);
182
+ return fieldOrder
135
183
  .filter(fieldId => {
136
- const prop = schema.schema.properties[fieldId];
184
+ const prop = properties[fieldId];
137
185
  if (!prop)
138
186
  return false;
139
187
  // Evaluate conditional visibility
@@ -141,8 +189,8 @@ const resolveFields = (schema, formValues) => {
141
189
  })
142
190
  .map(fieldId => ({
143
191
  key: fieldId,
144
- property: schema.schema.properties[fieldId],
145
- ui: schema.uiSchema[fieldId] || {},
192
+ property: properties[fieldId],
193
+ ui: uiSchema[fieldId] || {},
146
194
  required: requiredSet.has(fieldId),
147
195
  }));
148
196
  };
@@ -159,13 +207,16 @@ const getRegistrationFields = (schema, registrationConfig, formValues) => {
159
207
  if (!schema || !registrationConfig.length)
160
208
  return [];
161
209
  const configMap = new Map(registrationConfig.map(c => [c.key, c]));
162
- const requiredSet = new Set(schema.schema.required || []);
163
- return schema.fieldOrder
210
+ const fieldOrder = schema.fieldOrder ?? [];
211
+ const properties = schema.schema?.properties ?? {};
212
+ const uiSchema = schema.uiSchema ?? {};
213
+ const requiredSet = new Set(schema.schema?.required || []);
214
+ return fieldOrder
164
215
  .filter(fieldId => {
165
216
  const config = configMap.get(fieldId);
166
217
  if (!config?.showDuringRegistration)
167
218
  return false;
168
- const prop = schema.schema.properties[fieldId];
219
+ const prop = properties[fieldId];
169
220
  if (!prop)
170
221
  return false;
171
222
  return evaluateConditions(prop.conditions, prop.showWhen, formValues || {});
@@ -174,8 +225,8 @@ const getRegistrationFields = (schema, registrationConfig, formValues) => {
174
225
  const config = configMap.get(fieldId);
175
226
  return {
176
227
  key: fieldId,
177
- property: schema.schema.properties[fieldId],
178
- ui: schema.uiSchema[fieldId] || {},
228
+ property: properties[fieldId],
229
+ ui: uiSchema[fieldId] || {},
179
230
  required: config.required ?? requiredSet.has(fieldId),
180
231
  };
181
232
  });
@@ -263,7 +314,7 @@ const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading
263
314
  if (newMode !== mode) {
264
315
  onModeSwitch();
265
316
  }
266
- }, disabled: loading })), jsxs("div", { className: "auth-form-header", children: [jsx("h2", { className: "auth-form-title", children: title }), jsx("p", { className: "auth-form-subtitle", children: subtitle })] }), error && (jsxs("div", { className: "auth-error", role: "alert", children: [jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: jsx("path", { d: "M8 0C3.58 0 0 3.58 0 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z" }) }), error] })), mode === 'register' && (jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "displayName", className: "auth-label", children: "Full Name" }), jsx("input", { type: "text", id: "displayName", className: "auth-input", value: formData.displayName || '', onChange: (e) => handleChange('displayName', e.target.value), required: mode === 'register', disabled: loading, placeholder: "John Doe" })] })), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "email", className: "auth-label", children: "Email address" }), jsx("input", { type: "email", id: "email", className: "auth-input", value: formData.email || '', onChange: (e) => handleChange('email', e.target.value), required: true, disabled: loading, placeholder: "you@example.com", autoComplete: "email" })] }), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "password", className: "auth-label", children: "Password" }), jsx("input", { type: "password", id: "password", className: "auth-input", value: formData.password || '', onChange: (e) => handleChange('password', e.target.value), required: true, disabled: loading, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", autoComplete: mode === 'login' ? 'current-password' : 'new-password', minLength: 6 })] }), mode === 'register' && hasSchemaFields && schemaFields.inline.map(renderSchemaField), mode === 'register' && hasLegacyFields && additionalFields.map(renderLegacyField), mode === 'register' && hasSchemaFields && schemaFields.postCredentials.length > 0 && (jsxs(Fragment, { children: [jsx("div", { className: "auth-divider", style: { margin: '16px 0' }, children: jsx("span", { children: "Additional Information" }) }), schemaFields.postCredentials.map(renderSchemaField)] })), mode === 'login' && (jsx("div", { className: "auth-form-footer", children: jsx("button", { type: "button", className: "auth-link", onClick: onForgotPassword, disabled: loading, children: "Forgot password?" }) })), jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading, children: loading ? (jsx("span", { className: "auth-spinner" })) : mode === 'login' ? ('Sign in') : ('Create account') }), signupProminence !== 'balanced' && (jsxs("div", { className: "auth-divider", children: [jsx("span", { children: mode === 'login' ? "Don't have an account?" : 'Already have an account?' }), jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onModeSwitch, disabled: loading, children: mode === 'login' ? 'Sign up' : 'Sign in' })] }))] }));
317
+ }, disabled: loading })), jsxs("div", { className: "auth-form-header", children: [jsx("h2", { className: "auth-form-title", children: title }), jsx("p", { className: "auth-form-subtitle", children: subtitle })] }), error && (jsxs("div", { className: "auth-error", role: "alert", children: [jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: jsx("path", { d: "M8 0C3.58 0 0 3.58 0 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z" }) }), error] })), mode === 'register' && (jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "displayName", className: "auth-label", children: "Full Name" }), jsx("input", { type: "text", id: "displayName", className: "auth-input", value: formData.displayName || '', onChange: (e) => handleChange('displayName', e.target.value), required: mode === 'register', disabled: loading, placeholder: "John Smith" })] })), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "email", className: "auth-label", children: "Email address" }), jsx("input", { type: "email", id: "email", className: "auth-input", value: formData.email || '', onChange: (e) => handleChange('email', e.target.value), required: true, disabled: loading, placeholder: "you@example.com", autoComplete: "email" })] }), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "password", className: "auth-label", children: "Password" }), jsx("input", { type: "password", id: "password", className: "auth-input", value: formData.password || '', onChange: (e) => handleChange('password', e.target.value), required: true, disabled: loading, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", autoComplete: mode === 'login' ? 'current-password' : 'new-password', minLength: 6 })] }), mode === 'register' && hasSchemaFields && schemaFields.inline.map(renderSchemaField), mode === 'register' && hasLegacyFields && additionalFields.map(renderLegacyField), mode === 'register' && hasSchemaFields && schemaFields.postCredentials.length > 0 && (jsxs(Fragment, { children: [jsx("div", { className: "auth-divider", style: { margin: '16px 0' }, children: jsx("span", { children: "Additional Information" }) }), schemaFields.postCredentials.map(renderSchemaField)] })), mode === 'login' && (jsx("div", { className: "auth-form-footer", children: jsx("button", { type: "button", className: "auth-link", onClick: onForgotPassword, disabled: loading, children: "Forgot password?" }) })), jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading, children: loading ? (jsx("span", { className: "auth-spinner" })) : mode === 'login' ? ('Sign in') : ('Create account') }), signupProminence !== 'balanced' && (jsxs("div", { className: "auth-divider", children: [jsx("span", { children: mode === 'login' ? "Don't have an account?" : 'Already have an account?' }), jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onModeSwitch, disabled: loading, children: mode === 'login' ? 'Sign up' : 'Sign in' })] }))] }));
267
318
  };
268
319
 
269
320
  const ProviderButtons = ({ enabledProviders, providerOrder, onEmailLogin, onGoogleLogin, onPhoneLogin, onMagicLinkLogin, loading, }) => {
@@ -11681,15 +11732,27 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11681
11732
  const headers = getApiHeaders();
11682
11733
  const hasBearer = !!headers['Authorization'];
11683
11734
  const hasSdkProxy = isProxyEnabled();
11735
+ console.log('[AuthContext] 🔍 Proxy mode init:', {
11736
+ hasBearer,
11737
+ hasSdkProxy,
11738
+ authHeader: headers['Authorization'] ? `${headers['Authorization'].substring(0, 20)}...` : '(none)',
11739
+ willCallGetAccount: hasBearer || hasSdkProxy,
11740
+ });
11684
11741
  if (!hasBearer && !hasSdkProxy) {
11685
- console.debug('[AuthContext] Skipping getAccount - no credentials available');
11742
+ console.log('[AuthContext] ⏭️ Skipping getAccount - no bearer token and SDK proxy not enabled');
11686
11743
  // Fall through to "no valid session" state
11687
11744
  }
11688
11745
  else {
11746
+ console.log('[AuthContext] 📡 Calling auth.getAccount() via', hasSdkProxy ? 'proxy' : 'bearer');
11689
11747
  try {
11690
11748
  const accountResponse = await smartlinks.auth.getAccount();
11691
11749
  const accountAny = accountResponse;
11692
11750
  const hasValidSession = accountAny?.uid && accountAny.uid.length > 0;
11751
+ console.log('[AuthContext] 📋 getAccount response:', {
11752
+ hasValidSession,
11753
+ uid: accountAny?.uid || '(none)',
11754
+ email: accountAny?.email || '(none)',
11755
+ });
11693
11756
  if (hasValidSession && isMounted) {
11694
11757
  const userFromAccount = {
11695
11758
  uid: accountAny.uid,
@@ -11706,10 +11769,11 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11706
11769
  syncContactRef.current?.(userFromAccount, accountResponse);
11707
11770
  }
11708
11771
  else if (isMounted) {
11709
- // No valid session, awaiting login
11772
+ console.log('[AuthContext] ℹ️ No valid session found via proxy');
11710
11773
  }
11711
11774
  }
11712
11775
  catch (error) {
11776
+ console.warn('[AuthContext] ❌ getAccount() failed:', error);
11713
11777
  // auth.getAccount() failed, awaiting login
11714
11778
  }
11715
11779
  } // end else (has credentials)
@@ -12218,7 +12282,6 @@ const useAuth = () => {
12218
12282
 
12219
12283
  /**
12220
12284
  * Friendly error messages for common HTTP status codes.
12221
- * Maps status codes to context-specific user-friendly messages.
12222
12285
  */
12223
12286
  const STATUS_MESSAGES = {
12224
12287
  400: 'Invalid request. Please check your input and try again.',
@@ -12230,41 +12293,103 @@ const STATUS_MESSAGES = {
12230
12293
  };
12231
12294
  /**
12232
12295
  * Context-specific error messages for different auth operations.
12233
- * Use `errorCode` from the SDK when available, otherwise fall back to status code.
12234
12296
  */
12235
12297
  const ERROR_CODE_MESSAGES = {
12236
- // Auth-specific error codes
12298
+ // 400 - Validation errors
12299
+ 'MISSING_FIELDS': 'Email and password are required.',
12300
+ 'MISSING_EMAIL': 'Email is required.',
12301
+ 'MISSING_PASSWORD': 'Password is required.',
12302
+ 'MISSING_TOKEN': 'Token is required.',
12303
+ 'MISSING_PHONE_NUMBER': 'Phone number is required.',
12304
+ 'MISSING_VERIFICATION_CODE': 'Phone number and verification code are required.',
12305
+ 'MISSING_REDIRECT_URL': 'Redirect URL is required.',
12306
+ 'MISSING_GOOGLE_TOKEN': 'Google token is required.',
12307
+ 'INVALID_CLIENT_ID': 'Invalid client configuration.',
12308
+ 'INVALID_REDIRECT_URL': 'Invalid redirect URL.',
12309
+ 'INVALID_PHONE_NUMBER': 'Invalid phone number. Please check the format and try again.',
12310
+ 'INVALID_GOOGLE_TOKEN': 'Invalid Google sign-in token.',
12311
+ 'PASSWORD_TOO_SHORT': 'Password must be at least 8 characters long.',
12312
+ 'PASSWORD_REQUIREMENTS_NOT_MET': 'New password must be at least 6 characters.',
12313
+ 'EMAIL_ALREADY_VERIFIED': 'Your email is already verified.',
12314
+ 'INVALID_CONFIRMATION': 'Please type DELETE to confirm account deletion.',
12315
+ // 401 - Authentication errors
12237
12316
  'INVALID_CREDENTIALS': 'Invalid email or password.',
12238
- 'EMAIL_ALREADY_EXISTS': 'This email is already registered.',
12317
+ 'INCORRECT_PASSWORD': 'Current password is incorrect.',
12318
+ 'INVALID_VERIFICATION_CODE': 'Invalid or expired verification code. Please try again.',
12319
+ 'INVALID_TOKEN': 'This link has expired or is invalid. Please request a new one.',
12320
+ 'TOKEN_EXPIRED': 'This link has expired. Please request a new one.',
12321
+ 'TOKEN_ALREADY_USED': 'This link has already been used. Please request a new one.',
12322
+ 'UNAUTHORIZED': 'You must be logged in to perform this action.',
12323
+ 'GOOGLE_TOKEN_AUDIENCE_MISMATCH': 'Google sign-in failed. Token was not issued for this application.',
12324
+ // 403 - Forbidden / verification required
12239
12325
  'EMAIL_NOT_VERIFIED': 'Please verify your email before signing in.',
12240
- 'ACCOUNT_LOCKED': 'Your account has been locked. Please contact support.',
12326
+ 'ACCOUNT_LOCKED': 'Your account has been locked. Please verify your email to unlock.',
12327
+ 'EMAIL_VERIFICATION_EXPIRED': 'Your verification deadline has passed and your account is locked. Please contact support.',
12328
+ // 404
12329
+ 'USER_NOT_FOUND': 'Account not found. Please check your email or create a new account.',
12330
+ // 409 - Conflicts
12331
+ 'EMAIL_ALREADY_EXISTS': 'This email is already registered.',
12332
+ 'EMAIL_IN_USE': 'This email is already in use.',
12333
+ // 429 - Rate limiting
12334
+ 'RATE_LIMIT_EXCEEDED': 'Too many requests. Please try again later.',
12335
+ 'TOO_MANY_MAGIC_LINKS': 'Too many magic link requests. Please try again later.',
12336
+ 'TOO_MANY_VERIFICATION_ATTEMPTS': 'Too many verification attempts. Please wait and try again.',
12337
+ 'MAX_VERIFICATION_ATTEMPTS': 'Maximum verification attempts reached. Please try again later.',
12338
+ // 500 - Server errors
12339
+ 'LOGIN_FAILED': 'Login failed. Please try again later.',
12340
+ 'REGISTRATION_FAILED': 'Registration failed. Please try again later.',
12341
+ 'GOOGLE_AUTH_NOT_CONFIGURED': 'Google sign-in is not available for this application.',
12342
+ 'GOOGLE_AUTH_FAILED': 'Google sign-in failed. Please try again.',
12343
+ 'GOOGLE_USERINFO_FAILED': 'Failed to retrieve your Google account information. Please try again.',
12344
+ 'PHONE_VERIFICATION_FAILED': 'Phone verification failed. Please try again.',
12345
+ 'SEND_VERIFICATION_CODE_FAILED': 'Failed to send verification code. Please try again.',
12346
+ 'MAGIC_LINK_SEND_FAILED': 'Failed to send magic link. Please try again.',
12347
+ 'MAGIC_LINK_VERIFICATION_FAILED': 'Magic link verification failed. Please try again.',
12348
+ 'PASSWORD_RESET_FAILED': 'Failed to process password reset. Please try again.',
12349
+ 'PASSWORD_RESET_COMPLETE_FAILED': 'Failed to reset password. Please try again.',
12350
+ 'EMAIL_VERIFICATION_SEND_FAILED': 'Failed to send verification email. Please try again.',
12351
+ 'EMAIL_VERIFICATION_FAILED': 'Email verification failed. Please try again.',
12352
+ // Account management 500s
12353
+ 'UPDATE_PROFILE_FAILED': 'Failed to update profile. Please try again.',
12354
+ 'CHANGE_PASSWORD_FAILED': 'Failed to change password. Please try again.',
12355
+ 'CHANGE_EMAIL_FAILED': 'Failed to change email. Please try again.',
12356
+ 'UPDATE_PHONE_FAILED': 'Failed to update phone number. Please try again.',
12357
+ 'DELETE_ACCOUNT_FAILED': 'Failed to delete account. Please try again.',
12358
+ 'CONFIG_FETCH_FAILED': 'Failed to load configuration. Please try again.',
12359
+ 'INTERNAL_ERROR': 'An unexpected error occurred. Please try again.',
12360
+ // Legacy aliases (kept for backward compatibility)
12241
12361
  'INVALID_CODE': 'Invalid verification code. Please check and try again.',
12242
12362
  'CODE_EXPIRED': 'This code has expired. Please request a new one.',
12243
- 'INVALID_TOKEN': 'This link has expired or is invalid. Please request a new one.',
12244
- 'TOKEN_EXPIRED': 'This link has expired. Please request a new one.',
12245
12363
  'PHONE_NOT_SUPPORTED': 'This phone number is not supported. Please try a different number.',
12246
12364
  'INVALID_PHONE': 'Invalid phone number. Please check the format and try again.',
12247
12365
  'PASSWORD_TOO_WEAK': 'Password is too weak. Please use at least 8 characters with a mix of letters and numbers.',
12248
12366
  'RATE_LIMITED': 'Too many attempts. Please wait a moment and try again.',
12249
12367
  };
12368
+ function isApiErrorLike(error) {
12369
+ if (error instanceof SmartlinksApiError)
12370
+ return true;
12371
+ if (error && typeof error === 'object' && 'statusCode' in error && 'message' in error) {
12372
+ const e = error;
12373
+ return typeof e.statusCode === 'number' && typeof e.message === 'string';
12374
+ }
12375
+ return false;
12376
+ }
12250
12377
  /**
12251
12378
  * Extracts a user-friendly error message from an error.
12252
12379
  *
12253
12380
  * Handles:
12254
- * - SmartlinksApiError: Uses statusCode and errorCode for precise messaging
12381
+ * - SmartlinksApiError (and duck-typed equivalents from proxy mode)
12255
12382
  * - Standard Error: Uses message property
12256
12383
  * - String: Passes through directly (for native bridge errors)
12257
12384
  * - Unknown: Returns generic message
12258
- *
12259
- * @param error - The caught error (SmartlinksApiError, Error, string, or unknown)
12260
- * @returns A user-friendly error message suitable for display
12261
12385
  */
12262
12386
  function getFriendlyErrorMessage(error) {
12263
- // Handle SmartlinksApiError with structured data
12264
- if (error instanceof SmartlinksApiError) {
12387
+ // Handle SmartlinksApiError or duck-typed API errors (proxy mode)
12388
+ if (isApiErrorLike(error)) {
12265
12389
  // First, check for specific error code (most precise)
12266
- if (error.errorCode && ERROR_CODE_MESSAGES[error.errorCode]) {
12267
- return ERROR_CODE_MESSAGES[error.errorCode];
12390
+ const errorCode = error.errorCode || error.details?.errorCode || error.details?.error;
12391
+ if (errorCode && ERROR_CODE_MESSAGES[errorCode]) {
12392
+ return ERROR_CODE_MESSAGES[errorCode];
12268
12393
  }
12269
12394
  // Then, check status code for general category messages
12270
12395
  if (error.statusCode >= 500) {
@@ -12278,6 +12403,34 @@ function getFriendlyErrorMessage(error) {
12278
12403
  }
12279
12404
  // Handle standard Error objects
12280
12405
  if (error instanceof Error) {
12406
+ // SDK bug workaround: SDK may do `throw new Error(responseBodyObject)` which produces
12407
+ // message "[object Object]". Check for API error properties attached to the Error instance.
12408
+ const errAny = error;
12409
+ // Check if the Error has API error properties directly attached (e.g., error.statusCode, error.errorCode)
12410
+ if (typeof errAny.statusCode === 'number' || errAny.errorCode || errAny.response) {
12411
+ // Try to extract from attached properties
12412
+ const apiLike = errAny.response || errAny;
12413
+ if (isApiErrorLike(apiLike)) {
12414
+ return getFriendlyErrorMessage(apiLike);
12415
+ }
12416
+ }
12417
+ // Check if the Error has a `cause` with API error details (modern Error cause pattern)
12418
+ if (errAny.cause && typeof errAny.cause === 'object') {
12419
+ if (isApiErrorLike(errAny.cause)) {
12420
+ return getFriendlyErrorMessage(errAny.cause);
12421
+ }
12422
+ }
12423
+ // If the message is "[object Object]", the error was constructed from a plain object
12424
+ // This is useless - return a generic message instead
12425
+ if (error.message === '[object Object]') {
12426
+ // Log the actual error for debugging
12427
+ console.warn('[AuthKit] Error with [object Object] message. Raw error:', JSON.stringify(errAny, Object.getOwnPropertyNames(errAny)));
12428
+ return 'An unexpected error occurred. Please try again.';
12429
+ }
12430
+ // Check if the message itself contains a known API error pattern
12431
+ if (/already (registered|exists)/i.test(error.message)) {
12432
+ return 'This email is already registered.';
12433
+ }
12281
12434
  return error.message;
12282
12435
  }
12283
12436
  // Handle plain strings (e.g., from native bridge callbacks)
@@ -12291,41 +12444,57 @@ function getFriendlyErrorMessage(error) {
12291
12444
  * Checks if an error represents a conflict (409) - typically duplicate registration.
12292
12445
  */
12293
12446
  function isConflictError(error) {
12294
- return error instanceof SmartlinksApiError && error.statusCode === 409;
12447
+ if (isApiErrorLike(error))
12448
+ return error.statusCode === 409;
12449
+ // Also check error message for keyword match (resilient fallback)
12450
+ if (error instanceof Error && /already (registered|exists)/i.test(error.message))
12451
+ return true;
12452
+ return false;
12295
12453
  }
12296
12454
  /**
12297
12455
  * Checks if an error represents invalid credentials (401).
12298
12456
  */
12299
12457
  function isAuthError(error) {
12300
- return error instanceof SmartlinksApiError && error.isAuthError();
12458
+ if (error instanceof SmartlinksApiError)
12459
+ return error.isAuthError();
12460
+ if (isApiErrorLike(error))
12461
+ return error.statusCode === 401 || error.statusCode === 403;
12462
+ return false;
12301
12463
  }
12302
12464
  /**
12303
12465
  * Checks if an error represents rate limiting (429).
12304
12466
  */
12305
12467
  function isRateLimitError(error) {
12306
- return error instanceof SmartlinksApiError && error.isRateLimited();
12468
+ if (error instanceof SmartlinksApiError)
12469
+ return error.isRateLimited();
12470
+ if (isApiErrorLike(error))
12471
+ return error.statusCode === 429;
12472
+ return false;
12307
12473
  }
12308
12474
  /**
12309
12475
  * Checks if an error represents a server error (5xx).
12310
12476
  */
12311
12477
  function isServerError(error) {
12312
- return error instanceof SmartlinksApiError && error.isServerError();
12478
+ if (error instanceof SmartlinksApiError)
12479
+ return error.isServerError();
12480
+ if (isApiErrorLike(error))
12481
+ return error.statusCode >= 500;
12482
+ return false;
12313
12483
  }
12314
12484
  /**
12315
12485
  * Gets the HTTP status code from an error, if available.
12316
12486
  */
12317
12487
  function getErrorStatusCode(error) {
12318
- if (error instanceof SmartlinksApiError) {
12488
+ if (isApiErrorLike(error))
12319
12489
  return error.statusCode;
12320
- }
12321
12490
  return undefined;
12322
12491
  }
12323
12492
  /**
12324
12493
  * Gets the server-specific error code from an error, if available.
12325
12494
  */
12326
12495
  function getErrorCode(error) {
12327
- if (error instanceof SmartlinksApiError) {
12328
- return error.errorCode;
12496
+ if (isApiErrorLike(error)) {
12497
+ return error.errorCode || error.details?.errorCode || error.details?.error;
12329
12498
  }
12330
12499
  return undefined;
12331
12500
  }
@@ -12555,6 +12724,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12555
12724
  const [sdkReady, setSdkReady] = useState(false); // Track SDK initialization state
12556
12725
  const [contactSchema, setContactSchema] = useState(null); // Schema for registration fields
12557
12726
  const [silentSignInChecked, setSilentSignInChecked] = useState(false); // Track if silent sign-in has been checked
12727
+ const [googleFallbackToPopup, setGoogleFallbackToPopup] = useState(false); // Show popup fallback when FedCM is blocked/dismissed
12558
12728
  const log = useMemo(() => createLoggerWrapper(logger), [logger]);
12559
12729
  const api = new AuthAPI(apiEndpoint, clientId, clientName, logger);
12560
12730
  const auth = useAuth();
@@ -13087,6 +13257,8 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13087
13257
  }
13088
13258
  }
13089
13259
  catch (err) {
13260
+ // Debug: log the raw error shape to help diagnose SDK error wrapping issues
13261
+ log.error('handleEmailAuth error:', typeof err, err instanceof Error ? `Error.message=${err.message}` : '', JSON.stringify(err, Object.getOwnPropertyNames(err || {})));
13090
13262
  // Check if error is about email already registered (409 conflict)
13091
13263
  // Handle both SmartlinksApiError (statusCode 409) and plain Error with keyword matching
13092
13264
  if (mode === 'register' && (isConflictError(err) ||
@@ -13098,7 +13270,8 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13098
13270
  else {
13099
13271
  setError(getFriendlyErrorMessage(err));
13100
13272
  }
13101
- onAuthError?.(err instanceof Error ? err : new Error(getFriendlyErrorMessage(err)));
13273
+ const friendlyMsg = getFriendlyErrorMessage(err);
13274
+ onAuthError?.(err instanceof Error ? new Error(friendlyMsg) : new Error(friendlyMsg));
13102
13275
  }
13103
13276
  finally {
13104
13277
  setLoading(false);
@@ -13147,6 +13320,22 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13147
13320
  setLoading(false);
13148
13321
  }
13149
13322
  };
13323
+ // Retry Google login using popup flow (fallback when FedCM/OneTap is blocked)
13324
+ const handleGooglePopupFallback = () => {
13325
+ setGoogleFallbackToPopup(false);
13326
+ setError(undefined);
13327
+ // Temporarily override the flow to popup and call handleGoogleLogin
13328
+ const originalFlow = config?.googleOAuthFlow;
13329
+ if (config) {
13330
+ config.googleOAuthFlow = 'popup';
13331
+ }
13332
+ handleGoogleLogin().finally(() => {
13333
+ // Restore original flow setting
13334
+ if (config) {
13335
+ config.googleOAuthFlow = originalFlow;
13336
+ }
13337
+ });
13338
+ };
13150
13339
  const handleGoogleLogin = async () => {
13151
13340
  const hasCustomGoogleClientId = !!config?.googleClientId;
13152
13341
  const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
@@ -13162,6 +13351,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13162
13351
  const isWhitelisted = isWhitelistedGoogleDomain(config?.whitelistedGoogleDomains);
13163
13352
  const googleProxyUrl = config?.googleOAuthProxyUrl
13164
13353
  || (!hasCustomGoogleClientId && !isWhitelisted ? DEFAULT_GOOGLE_PROXY_URL : undefined);
13354
+ setGoogleFallbackToPopup(false);
13165
13355
  log.log('Google Auth initiated:', {
13166
13356
  configuredFlow,
13167
13357
  effectiveFlow: oauthFlow,
@@ -13472,14 +13662,26 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13472
13662
  cancel_on_tap_outside: true,
13473
13663
  use_fedcm_for_prompt: true, // Enable FedCM for future browser compatibility
13474
13664
  });
13475
- // Use timeout fallback instead of deprecated notification methods
13476
- // (isNotDisplayed/isSkippedMoment will stop working when FedCM becomes mandatory)
13665
+ // Use timeout fallback if no prompt interaction after 5s, assume FedCM was blocked
13477
13666
  const promptTimeout = setTimeout(() => {
13667
+ log.log('Google OneTap prompt timed out — FedCM may be blocked or unavailable');
13668
+ setGoogleFallbackToPopup(true);
13478
13669
  setLoading(false);
13479
13670
  }, 5000);
13480
- google.accounts.id.prompt(() => {
13481
- // Clear timeout if prompt interaction occurs
13671
+ google.accounts.id.prompt((notification) => {
13482
13672
  clearTimeout(promptTimeout);
13673
+ // Check for FedCM/OneTap dismissal or blocking
13674
+ // notification may have getMomentType(), getDismissedReason(), getSkippedReason()
13675
+ const momentType = notification?.getMomentType?.();
13676
+ const dismissedReason = notification?.getDismissedReason?.();
13677
+ const skippedReason = notification?.getSkippedReason?.();
13678
+ log.log('Google OneTap prompt notification:', { momentType, dismissedReason, skippedReason });
13679
+ if (momentType === 'skipped' || momentType === 'dismissed') {
13680
+ // User dismissed the prompt, or browser blocked it (FedCM disabled)
13681
+ // Offer popup flow as alternative
13682
+ log.log('Google OneTap was dismissed/skipped, offering popup fallback');
13683
+ setGoogleFallbackToPopup(true);
13684
+ }
13483
13685
  setLoading(false);
13484
13686
  });
13485
13687
  }
@@ -13802,33 +14004,68 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13802
14004
  fontSize: '0.875rem',
13803
14005
  fontWeight: 500,
13804
14006
  opacity: loading ? 0.6 : 1
13805
- }, children: "Cancel" })] })] })) : (jsx(Fragment, { children: (() => {
13806
- const emailDisplayMode = config?.emailDisplayMode || 'form';
13807
- const providerOrder = config?.providerOrder || (config?.enabledProviders || enabledProviders);
13808
- const actualProviders = config?.enabledProviders || enabledProviders;
13809
- // Button mode: show provider selection first, then email form if email is selected
13810
- if (emailDisplayMode === 'button' && !showEmailForm) {
13811
- return (jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onEmailLogin: () => setShowEmailForm(true), onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }));
13812
- }
13813
- // Form mode or email button was clicked: show email form with other providers
13814
- return (jsxs(Fragment, { children: [emailDisplayMode === 'button' && showEmailForm && (jsx("button", { onClick: () => setShowEmailForm(false), style: {
13815
- marginBottom: '1rem',
13816
- padding: '0.5rem',
13817
- background: 'none',
13818
- border: 'none',
13819
- color: 'var(--auth-text-color, #6B7280)',
13820
- cursor: 'pointer',
14007
+ }, children: "Cancel" })] })] })) : (jsxs(Fragment, { children: [googleFallbackToPopup && (jsxs("div", { style: {
14008
+ marginBottom: '1rem',
14009
+ padding: '0.75rem 1rem',
14010
+ borderRadius: '0.5rem',
14011
+ backgroundColor: resolvedTheme === 'dark' ? 'rgba(59, 130, 246, 0.1)' : 'rgba(59, 130, 246, 0.05)',
14012
+ border: `1px solid ${resolvedTheme === 'dark' ? 'rgba(59, 130, 246, 0.3)' : 'rgba(59, 130, 246, 0.2)'}`,
14013
+ }, children: [jsx("p", { style: {
14014
+ fontSize: '0.8125rem',
14015
+ color: resolvedTheme === 'dark' ? '#93c5fd' : '#1e40af',
14016
+ marginBottom: '0.5rem',
14017
+ lineHeight: 1.4,
14018
+ }, children: "Google sign-in was blocked by your browser. You can try signing in via a popup window instead." }), jsxs("button", { onClick: handleGooglePopupFallback, disabled: loading, style: {
14019
+ width: '100%',
14020
+ padding: '0.5rem 1rem',
13821
14021
  fontSize: '0.875rem',
14022
+ fontWeight: 500,
14023
+ color: '#fff',
14024
+ backgroundColor: '#4285F4',
14025
+ border: 'none',
14026
+ borderRadius: '0.375rem',
14027
+ cursor: loading ? 'not-allowed' : 'pointer',
14028
+ opacity: loading ? 0.6 : 1,
13822
14029
  display: 'flex',
13823
14030
  alignItems: 'center',
13824
- gap: '0.25rem'
13825
- }, children: "\u2190 Back to options" })), jsx(EmailAuthForm, { mode: mode, onSubmit: handleEmailAuth, onModeSwitch: () => {
13826
- setMode(mode === 'login' ? 'register' : 'login');
13827
- setShowResendVerification(false);
13828
- setShowRequestNewReset(false);
13829
- setError(undefined);
13830
- }, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error, signupProminence: resolvedSignupProminence, schema: contactSchema, registrationFieldsConfig: config?.registrationFields, 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 }))] }));
13831
- })() })) })) : null }));
14031
+ justifyContent: 'center',
14032
+ gap: '0.5rem',
14033
+ }, children: [jsxs("svg", { width: "18", height: "18", viewBox: "0 0 18 18", xmlns: "http://www.w3.org/2000/svg", children: [jsx("path", { d: "M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844a4.14 4.14 0 0 1-1.796 2.716v2.259h2.908c1.702-1.567 2.684-3.875 2.684-6.615Z", fill: "#4285F4" }), jsx("path", { d: "M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18Z", fill: "#34A853" }), jsx("path", { d: "M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.997 8.997 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.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 0A8.997 8.997 0 0 0 .957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58Z", fill: "#EA4335" })] }), "Sign in with Google"] }), jsx("button", { onClick: () => setGoogleFallbackToPopup(false), style: {
14034
+ marginTop: '0.375rem',
14035
+ width: '100%',
14036
+ padding: '0.25rem',
14037
+ fontSize: '0.75rem',
14038
+ color: resolvedTheme === 'dark' ? '#64748b' : '#9ca3af',
14039
+ backgroundColor: 'transparent',
14040
+ border: 'none',
14041
+ cursor: 'pointer',
14042
+ }, children: "Dismiss" })] })), (() => {
14043
+ const emailDisplayMode = config?.emailDisplayMode || 'form';
14044
+ const providerOrder = config?.providerOrder || (config?.enabledProviders || enabledProviders);
14045
+ const actualProviders = config?.enabledProviders || enabledProviders;
14046
+ // Button mode: show provider selection first, then email form if email is selected
14047
+ if (emailDisplayMode === 'button' && !showEmailForm) {
14048
+ return (jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onEmailLogin: () => setShowEmailForm(true), onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }));
14049
+ }
14050
+ // Form mode or email button was clicked: show email form with other providers
14051
+ return (jsxs(Fragment, { children: [emailDisplayMode === 'button' && showEmailForm && (jsx("button", { onClick: () => setShowEmailForm(false), style: {
14052
+ marginBottom: '1rem',
14053
+ padding: '0.5rem',
14054
+ background: 'none',
14055
+ border: 'none',
14056
+ color: 'var(--auth-text-color, #6B7280)',
14057
+ cursor: 'pointer',
14058
+ fontSize: '0.875rem',
14059
+ display: 'flex',
14060
+ alignItems: 'center',
14061
+ gap: '0.25rem'
14062
+ }, children: "\u2190 Back to options" })), jsx(EmailAuthForm, { mode: mode, onSubmit: handleEmailAuth, onModeSwitch: () => {
14063
+ setMode(mode === 'login' ? 'register' : 'login');
14064
+ setShowResendVerification(false);
14065
+ setShowRequestNewReset(false);
14066
+ setError(undefined);
14067
+ }, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error, signupProminence: resolvedSignupProminence, schema: contactSchema, registrationFieldsConfig: config?.registrationFields, 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 }))] }));
14068
+ })()] })) })) : null }));
13832
14069
  };
13833
14070
 
13834
14071
  var SmartlinksAuthUI$1 = /*#__PURE__*/Object.freeze({
@@ -14897,5 +15134,5 @@ async function setDefaultAuthKitId(collectionId, authKitId) {
14897
15134
  });
14898
15135
  }
14899
15136
 
14900
- export { AccountManagement, AuthProvider, AuthUIPreview, SmartlinksAuthUI as FirebaseAuthUI, ProtectedRoute, SchemaFieldRenderer, SmartlinksAuthUI, SmartlinksFrame, buildIframeSrc, getDefaultAuthKitId, getEditableFields, getErrorCode, getErrorStatusCode, getFriendlyErrorMessage, getRegistrationFields, isAdminFromRoles, isAuthError, isConflictError, isRateLimitError, isServerError, resolveFields, setDefaultAuthKitId, sortFieldsByPlacement, tokenStorage, useAdminDetection, useAuth, useIframeMessages, useIframeResize };
15137
+ export { AccountManagement, AuthProvider, AuthUIPreview, SmartlinksAuthUI as FirebaseAuthUI, ProtectedRoute, SchemaFieldRenderer, SmartlinksAuthUI, SmartlinksFrame, buildIframeSrc, evaluateConditions, getDefaultAuthKitId, getEditableFields, getErrorCode, getErrorStatusCode, getFriendlyErrorMessage, getRegistrationFields, isAdminFromRoles, isAuthError, isConflictError, isRateLimitError, isServerError, resolveFields, setDefaultAuthKitId, sortFieldsByPlacement, tokenStorage, useAdminDetection, useAuth, useIframeMessages, useIframeResize };
14901
15138
  //# sourceMappingURL=index.esm.js.map