@proveanything/smartlinks-auth-ui 0.3.10 → 0.3.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.
@@ -1 +1 @@
1
- {"version":3,"file":"AccountManagement.d.ts","sourceRoot":"","sources":["../../src/components/AccountManagement.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2C,MAAM,OAAO,CAAC;AAMhE,OAAO,KAAK,EAAE,sBAAsB,EAAe,MAAM,UAAU,CAAC;AACpE,OAAO,qBAAqB,CAAC;AAO7B,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CA0vB9D,CAAC"}
1
+ {"version":3,"file":"AccountManagement.d.ts","sourceRoot":"","sources":["../../src/components/AccountManagement.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2C,MAAM,OAAO,CAAC;AAMhE,OAAO,KAAK,EAAE,sBAAsB,EAAe,MAAM,UAAU,CAAC;AACpE,OAAO,qBAAqB,CAAC;AAO7B,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CA0wB9D,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"SmartlinksAuthUI.d.ts","sourceRoot":"","sources":["../../src/components/SmartlinksAuthUI.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAY5D,OAAO,KAAK,EAAE,qBAAqB,EAAyF,MAAM,UAAU,CAAC;AAkH7I,QAAA,MAAM,mBAAmB,QAAa,OAAO,CAAC,IAAI,CAmCjD,CAAC;AAwEF,OAAO,EAAE,mBAAmB,EAAE,CAAC;AAK/B,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CAyhD5D,CAAC"}
1
+ {"version":3,"file":"SmartlinksAuthUI.d.ts","sourceRoot":"","sources":["../../src/components/SmartlinksAuthUI.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAY5D,OAAO,KAAK,EAAE,qBAAqB,EAAyF,MAAM,UAAU,CAAC;AA0I7I,QAAA,MAAM,mBAAmB,QAAa,OAAO,CAAC,IAAI,CAmCjD,CAAC;AAwEF,OAAO,EAAE,mBAAmB,EAAE,CAAC;AAK/B,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CA0mD5D,CAAC"}
@@ -1,35 +1,5 @@
1
1
  import React from 'react';
2
- import type { ContactResponse } from '@proveanything/smartlinks';
3
- import type { AuthUser, AuthStateChangeCallback, AuthProviderProps } from '../types';
4
- interface AuthContextValue {
5
- user: AuthUser | null;
6
- token: string | null;
7
- accountData: Record<string, any> | null;
8
- accountInfo: Record<string, any> | null;
9
- isAuthenticated: boolean;
10
- isVerified: boolean;
11
- isLoading: boolean;
12
- isOnline: boolean;
13
- proxyMode: boolean;
14
- contact: ContactResponse | null;
15
- contactId: string | null;
16
- getContact: (forceRefresh?: boolean) => Promise<ContactResponse | null>;
17
- updateContactCustomFields: (customFields: Record<string, any>) => Promise<ContactResponse>;
18
- login: (token: string, user: AuthUser, accountData?: Record<string, any>, isNewUser?: boolean, expiresAt?: number) => Promise<void>;
19
- logout: () => Promise<void>;
20
- getToken: () => Promise<string | null>;
21
- getTokenInfo: () => Promise<{
22
- token: string;
23
- expiresAt: number;
24
- expiresIn: number;
25
- } | null>;
26
- refreshToken: () => Promise<string>;
27
- getAccount: (forceRefresh?: boolean) => Promise<Record<string, any>>;
28
- refreshAccount: () => Promise<Record<string, any>>;
29
- clearAccountCache: () => Promise<void>;
30
- onAuthStateChange: (callback: AuthStateChangeCallback) => () => void;
31
- retryVerification: () => Promise<boolean>;
32
- }
2
+ import type { AuthProviderProps, AuthContextValue } from '../types';
33
3
  export declare const AuthContext: React.Context<AuthContextValue | undefined>;
34
4
  export type { AuthContextValue };
35
5
  export declare const AuthProvider: React.FC<AuthProviderProps>;
@@ -1 +1 @@
1
- {"version":3,"file":"AuthContext.d.ts","sourceRoot":"","sources":["../../src/context/AuthContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8E,MAAM,OAAO,CAAC;AAGnG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAGjE,OAAO,KAAK,EAAE,QAAQ,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAErF,UAAU,gBAAgB;IACxB,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC;IACxC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC;IACxC,eAAe,EAAE,OAAO,CAAC;IACzB,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IAGnB,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IAChC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,CAAC,YAAY,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;IACxE,yBAAyB,EAAE,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;IAG3F,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpI,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,QAAQ,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,YAAY,EAAE,MAAM,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IAC5F,YAAY,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACpC,UAAU,EAAE,CAAC,YAAY,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IACrE,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IACnD,iBAAiB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,iBAAiB,EAAE,CAAC,QAAQ,EAAE,uBAAuB,KAAK,MAAM,IAAI,CAAC;IACrE,iBAAiB,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3C;AAGD,eAAO,MAAM,WAAW,6CAAyD,CAAC;AAGlF,YAAY,EAAE,gBAAgB,EAAE,CAAC;AAEjC,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CA0yBpD,CAAC;AAEF,eAAO,MAAM,OAAO,QAAO,gBAM1B,CAAC"}
1
+ {"version":3,"file":"AuthContext.d.ts","sourceRoot":"","sources":["../../src/context/AuthContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8E,MAAM,OAAO,CAAC;AAMnG,OAAO,KAAK,EAAqC,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGvG,eAAO,MAAM,WAAW,6CAAyD,CAAC;AAGlF,YAAY,EAAE,gBAAgB,EAAE,CAAC;AAEjC,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAq0BpD,CAAC;AAEF,eAAO,MAAM,OAAO,QAAO,gBAM1B,CAAC"}
package/dist/index.esm.js CHANGED
@@ -11530,21 +11530,48 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11530
11530
  return null;
11531
11531
  }
11532
11532
  }, [collectionId, shouldSyncContacts, proxyMode, token, accountData, accountInfo, isVerified, notifyAuthStateChange]);
11533
+ // Detect legacy vs new interactionConfig shape
11534
+ const isLegacyConfig = interactionConfig && !('interactionId' in interactionConfig) && ('login' in interactionConfig || 'logout' in interactionConfig || 'signup' in interactionConfig);
11535
+ if (isLegacyConfig) {
11536
+ console.warn('[AuthContext] Deprecated: interactionConfig uses legacy per-event-type IDs. Migrate to { interactionId, scopes? } shape. Legacy support will be removed in a future release.');
11537
+ }
11533
11538
  // Track interaction event (non-blocking)
11534
- // IMPORTANT: Only tracks if an explicit interaction ID is configured for the event type.
11535
- // Interaction IDs must be pre-created in SmartLinks - you cannot use arbitrary IDs.
11539
+ // Uses scope-based architecture: single interactionId + scope field to differentiate events.
11536
11540
  const trackInteraction = useCallback(async (eventType, userId, currentContactId, metadata) => {
11537
11541
  if (!collectionId || !shouldTrackInteractions)
11538
11542
  return;
11539
- const interactionIdMap = {
11540
- login: interactionConfig?.login,
11541
- logout: interactionConfig?.logout,
11542
- signup: interactionConfig?.signup,
11543
- session_restore: interactionConfig?.sessionRestore,
11544
- };
11545
- const interactionId = interactionIdMap[eventType];
11546
- if (!interactionId)
11543
+ let interactionId;
11544
+ let scope;
11545
+ if (isLegacyConfig) {
11546
+ // Legacy: per-event-type IDs (deprecated)
11547
+ const legacyConfig = interactionConfig;
11548
+ const legacyMap = {
11549
+ login: legacyConfig?.login,
11550
+ logout: legacyConfig?.logout,
11551
+ signup: legacyConfig?.signup,
11552
+ session_restore: legacyConfig?.sessionRestore,
11553
+ };
11554
+ interactionId = legacyMap[eventType];
11555
+ if (!interactionId)
11556
+ return;
11557
+ }
11558
+ else if (interactionConfig && 'interactionId' in interactionConfig) {
11559
+ // New: single ID + scope
11560
+ interactionId = interactionConfig.interactionId;
11561
+ if (!interactionId)
11562
+ return;
11563
+ const defaultScopes = {
11564
+ login: 'login',
11565
+ logout: 'logout',
11566
+ signup: 'signup',
11567
+ session_restore: 'session_restore',
11568
+ };
11569
+ const customScopes = interactionConfig.scopes || {};
11570
+ scope = customScopes[eventType === 'session_restore' ? 'sessionRestore' : eventType] || defaultScopes[eventType];
11571
+ }
11572
+ else {
11547
11573
  return;
11574
+ }
11548
11575
  try {
11549
11576
  await smartlinks.interactions.submitPublicEvent(collectionId, {
11550
11577
  collectionId,
@@ -11552,7 +11579,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11552
11579
  userId,
11553
11580
  contactId: currentContactId || undefined,
11554
11581
  appId: interactionAppId,
11555
- eventType,
11582
+ scope,
11556
11583
  outcome: 'completed',
11557
11584
  metadata: {
11558
11585
  ...metadata,
@@ -11565,7 +11592,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11565
11592
  catch (err) {
11566
11593
  // Non-blocking
11567
11594
  }
11568
- }, [collectionId, shouldTrackInteractions, interactionAppId, interactionConfig, user, token, accountData, accountInfo, isVerified, contact, contactId, notifyAuthStateChange]);
11595
+ }, [collectionId, shouldTrackInteractions, interactionAppId, interactionConfig, isLegacyConfig, user, token, accountData, accountInfo, isVerified, contact, contactId, notifyAuthStateChange]);
11569
11596
  // Keep refs in sync with latest callbacks (avoids stale closures)
11570
11597
  useEffect(() => {
11571
11598
  syncContactRef.current = syncContact;
@@ -11670,7 +11697,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11670
11697
  pendingVerificationRef.current = false;
11671
11698
  notifyAuthStateChange('SESSION_VERIFIED', storedUser, storedToken.token, storedAccountData || null, null, true, null, storedContactId);
11672
11699
  // Track session restore interaction (optional) - use ref for stable dependency
11673
- if (interactionConfig?.sessionRestore) {
11700
+ if (interactionConfig && ('interactionId' in interactionConfig || interactionConfig?.sessionRestore)) {
11674
11701
  trackInteractionRef.current?.('session_restore', storedUser.uid, storedContactId);
11675
11702
  }
11676
11703
  }
@@ -11716,7 +11743,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11716
11743
  return () => {
11717
11744
  isMounted = false;
11718
11745
  };
11719
- }, [proxyMode, notifyAuthStateChange, isNetworkError, interactionConfig?.sessionRestore]);
11746
+ }, [proxyMode, notifyAuthStateChange, isNetworkError, interactionConfig]);
11720
11747
  // Listen for parent auth state changes (proxy mode only)
11721
11748
  useEffect(() => {
11722
11749
  if (!proxyMode)
@@ -12085,9 +12112,9 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
12085
12112
  if (!storedToken?.expiresAt)
12086
12113
  return;
12087
12114
  const now = Date.now();
12088
- const tokenLifetime = storedToken.expiresAt - (storedToken.expiresAt - (7 * 24 * 60 * 60 * 1000));
12089
- const tokenAge = now - (storedToken.expiresAt - (7 * 24 * 60 * 60 * 1000));
12090
- const percentUsed = (tokenAge / tokenLifetime) * 100;
12115
+ const remainingMs = storedToken.expiresAt - now;
12116
+ const totalLifetimeMs = 7 * 24 * 60 * 60 * 1000;
12117
+ const percentUsed = totalLifetimeMs > 0 ? ((totalLifetimeMs - remainingMs) / totalLifetimeMs) * 100 : 100;
12091
12118
  if (percentUsed >= refreshThresholdPercent) {
12092
12119
  try {
12093
12120
  await refreshToken();
@@ -12283,6 +12310,25 @@ const getExpirationFromResponse = (response) => {
12283
12310
  };
12284
12311
  // Default Smartlinks Google OAuth Client ID (public - safe to expose)
12285
12312
  const DEFAULT_GOOGLE_CLIENT_ID = '696509063554-jdlbjl8vsjt7cr0vgkjkjf3ffnvi3a70.apps.googleusercontent.com';
12313
+ // Default Google OAuth proxy URL (hosted on our whitelisted domain)
12314
+ const DEFAULT_GOOGLE_PROXY_URL = 'https://smartlinks-auth-kit.lovable.app/google-proxy.html';
12315
+ // Exact hostnames where Google OAuth is registered and inline/OneTap flow works directly.
12316
+ // Only specific registered origins — NOT broad wildcards like *.lovable.app
12317
+ const WHITELISTED_GOOGLE_OAUTH_HOSTS = [
12318
+ 'smartlinks-auth-kit.lovable.app', // This app's dev/preview domain (registered in Google Console)
12319
+ 'smartlinks.app', // Production root
12320
+ 'localhost', // Local dev
12321
+ '127.0.0.1', // Local dev
12322
+ ];
12323
+ /**
12324
+ * Check if the current domain is whitelisted for direct Google OAuth.
12325
+ * Uses exact hostname match (plus subdomain match for smartlinks.app production).
12326
+ * Returns true if OneTap/inline flow can work without a proxy.
12327
+ */
12328
+ const isWhitelistedGoogleDomain = () => {
12329
+ const hostname = window.location.hostname;
12330
+ return WHITELISTED_GOOGLE_OAUTH_HOSTS.some(domain => hostname === domain || hostname.endsWith(`.${domain}`));
12331
+ };
12286
12332
  // Default auth UI configuration when no clientId is provided
12287
12333
  const DEFAULT_AUTH_CONFIG = {
12288
12334
  branding: {
@@ -13051,16 +13097,29 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13051
13097
  }
13052
13098
  };
13053
13099
  const handleGoogleLogin = async () => {
13100
+ const hasCustomGoogleClientId = !!config?.googleClientId;
13054
13101
  const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
13055
13102
  const configuredFlow = config?.googleOAuthFlow || 'oneTap';
13056
13103
  const isWebView = detectWebView();
13057
13104
  const nativeBridge = getNativeBridge();
13058
13105
  const oauthFlow = (configuredFlow === 'oneTap' && isWebView && !nativeBridge) ? 'redirect' : configuredFlow;
13106
+ // Auto-detect proxy need:
13107
+ // - If explicitly configured, use that URL
13108
+ // - If user has their own Google Client ID, they've registered their domains — no proxy needed
13109
+ // - If on a whitelisted SmartLinks domain, inline flow works directly
13110
+ // - Otherwise, auto-use the default proxy URL
13111
+ const isWhitelisted = isWhitelistedGoogleDomain();
13112
+ const googleProxyUrl = config?.googleOAuthProxyUrl
13113
+ || (!hasCustomGoogleClientId && !isWhitelisted ? DEFAULT_GOOGLE_PROXY_URL : undefined);
13059
13114
  log.log('Google Auth initiated:', {
13060
13115
  configuredFlow,
13061
13116
  effectiveFlow: oauthFlow,
13062
13117
  isWebView,
13063
13118
  hasNativeBridge: !!nativeBridge,
13119
+ hasCustomGoogleClientId,
13120
+ isWhitelistedDomain: isWhitelisted,
13121
+ usingProxy: !!googleProxyUrl,
13122
+ proxyUrl: googleProxyUrl,
13064
13123
  });
13065
13124
  setLoading(true);
13066
13125
  setError(undefined);
@@ -13135,6 +13194,70 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13135
13194
  setLoading(false);
13136
13195
  return;
13137
13196
  }
13197
+ // Priority 3: Google OAuth Proxy popup (for custom domains not registered in Google Console)
13198
+ if (googleProxyUrl) {
13199
+ log.log('Using Google OAuth proxy popup:', googleProxyUrl);
13200
+ const proxyParams = new URLSearchParams({
13201
+ clientId,
13202
+ returnOrigin: window.location.origin,
13203
+ ...(apiEndpoint ? { apiEndpoint } : {}),
13204
+ });
13205
+ const popupUrl = `${googleProxyUrl}?${proxyParams.toString()}`;
13206
+ const popup = window.open(popupUrl, 'google-oauth-proxy', 'width=500,height=600,menubar=no,toolbar=no,location=yes');
13207
+ if (!popup) {
13208
+ setError('Popup blocked. Please allow popups for this site and try again.');
13209
+ setLoading(false);
13210
+ return;
13211
+ }
13212
+ // Listen for result from proxy popup
13213
+ const handleProxyMessage = async (event) => {
13214
+ // Validate origin matches proxy URL
13215
+ try {
13216
+ const proxyOrigin = new URL(googleProxyUrl).origin;
13217
+ if (event.origin !== proxyOrigin)
13218
+ return;
13219
+ }
13220
+ catch {
13221
+ return;
13222
+ }
13223
+ if (event.data?.type !== 'smartlinks:google-proxy:result')
13224
+ return;
13225
+ window.removeEventListener('message', handleProxyMessage);
13226
+ clearInterval(checkClosed);
13227
+ try {
13228
+ if (event.data.success && event.data.token) {
13229
+ const { token, user, accountData, isNewUser, expiresAt, expiresIn } = event.data;
13230
+ const expiration = expiresAt || (expiresIn ? Date.now() + expiresIn : undefined);
13231
+ await auth.login(token, user, accountData, isNewUser, expiration);
13232
+ setAuthSuccess(true);
13233
+ setSuccessMessage('Google login successful!');
13234
+ onAuthSuccess(token, user, accountData);
13235
+ }
13236
+ else {
13237
+ const errorMsg = event.data.error || 'Google login failed';
13238
+ setError(getFriendlyErrorMessage(errorMsg));
13239
+ onAuthError?.(new Error(errorMsg));
13240
+ }
13241
+ }
13242
+ catch (err) {
13243
+ setError(getFriendlyErrorMessage(err));
13244
+ onAuthError?.(err instanceof Error ? err : new Error(getFriendlyErrorMessage(err)));
13245
+ }
13246
+ finally {
13247
+ setLoading(false);
13248
+ }
13249
+ };
13250
+ window.addEventListener('message', handleProxyMessage);
13251
+ // Monitor popup close without result (user closed it)
13252
+ const checkClosed = setInterval(() => {
13253
+ if (popup.closed) {
13254
+ clearInterval(checkClosed);
13255
+ window.removeEventListener('message', handleProxyMessage);
13256
+ setLoading(false);
13257
+ }
13258
+ }, 500);
13259
+ return; // Don't set loading to false - waiting for popup
13260
+ }
13138
13261
  log.log('Using web-based OAuth flow:', oauthFlow);
13139
13262
  // Priority 3: Web-based flows (redirect, popup, oneTap)
13140
13263
  // Dynamically load Google Identity Services if not already loaded
@@ -13368,7 +13491,6 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13368
13491
  if (resetToken && confirmPassword) {
13369
13492
  // Complete password reset with token
13370
13493
  await api.completePasswordReset(resetToken, emailOrPassword);
13371
- await api.completePasswordReset(resetToken, emailOrPassword);
13372
13494
  // Auto-login with the new password if we have the email
13373
13495
  if (resetEmail) {
13374
13496
  try {
@@ -13693,6 +13815,12 @@ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, class
13693
13815
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
13694
13816
  const { showProfileSection = true, showEmailSection = true, showPasswordSection = true, showPhoneSection = true, showDeleteAccount = false, showCustomFields = true, // New: show schema-driven custom fields
13695
13817
  } = customization;
13818
+ // Provider awareness helpers
13819
+ // If providers is undefined (backend not updated yet), fall back to showing everything
13820
+ const providers = profile?.providers;
13821
+ const hasPassword = profile?.hasPassword ?? (providers === undefined ? undefined : providers?.includes('password'));
13822
+ const isGoogleOnly = providers !== undefined && providers.includes('google.com') && !providers.includes('password');
13823
+ const isPhoneOnly = providers !== undefined && providers.includes('phone') && providers.length === 1;
13696
13824
  // Reinitialize Smartlinks SDK when apiEndpoint changes
13697
13825
  useEffect(() => {
13698
13826
  if (apiEndpoint) {
@@ -13710,13 +13838,11 @@ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, class
13710
13838
  const loadSchema = async () => {
13711
13839
  setSchemaLoading(true);
13712
13840
  try {
13713
- console.log('[AccountManagement] Loading schema for collection:', collectionId);
13714
13841
  const schemaResult = await smartlinks.contact.publicGetSchema(collectionId);
13715
- console.log('[AccountManagement] Schema loaded:', schemaResult);
13716
13842
  setSchema(schemaResult);
13717
13843
  }
13718
13844
  catch (err) {
13719
- console.warn('[AccountManagement] Failed to load schema:', err);
13845
+ // Non-fatal - component works without schema
13720
13846
  // Non-fatal - component works without schema
13721
13847
  }
13722
13848
  finally {
@@ -13731,7 +13857,6 @@ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, class
13731
13857
  // where getContact() is called before the server knows about the user.
13732
13858
  useEffect(() => {
13733
13859
  if (!auth.isAuthenticated || !auth.isVerified) {
13734
- console.log('[AccountManagement] Waiting for verified authentication before loading profile...');
13735
13860
  return;
13736
13861
  }
13737
13862
  loadProfile();
@@ -13832,7 +13957,7 @@ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, class
13832
13957
  setError(undefined);
13833
13958
  setSuccess(undefined);
13834
13959
  try {
13835
- console.log('[AccountManagement] Updating custom fields:', customFieldValues);
13960
+ await auth.updateContactCustomFields?.(customFieldValues);
13836
13961
  await auth.updateContactCustomFields?.(customFieldValues);
13837
13962
  setSuccess('Profile updated successfully!');
13838
13963
  setEditingSection(null);
@@ -13958,7 +14083,8 @@ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, class
13958
14083
  setError('Please type DELETE to confirm account deletion');
13959
14084
  return;
13960
14085
  }
13961
- if (!deletePassword) {
14086
+ // Only require password for users who have one
14087
+ if (hasPassword !== false && !deletePassword) {
13962
14088
  setError('Password is required');
13963
14089
  return;
13964
14090
  }
@@ -13988,7 +14114,7 @@ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, class
13988
14114
  }
13989
14115
  return (jsxs("div", { className: `account-management ${className}`, style: { maxWidth: '600px' }, children: [error && (jsx("div", { className: "auth-error", role: "alert", children: error })), success && (jsx("div", { className: "auth-success", role: "alert", children: success })), showProfileSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Display Name" }), jsx("div", { className: "field-value", children: profile?.displayName || 'Not set' })] }), editingSection !== 'profile' && (jsx("button", { type: "button", onClick: () => setEditingSection('profile'), className: "auth-button button-secondary", children: "Change" }))] }), editingSection === 'profile' && (jsxs("form", { onSubmit: handleUpdateProfile, className: "edit-form", children: [jsx("div", { className: "form-group", children: jsx("input", { id: "displayName", type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "Your display name", className: "auth-input" }) }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showCustomFields && collectionId && editableCustomFields.length > 0 && (jsxs("section", { className: "account-section", children: [jsx("h3", { className: "section-title", style: { fontSize: '0.9rem', fontWeight: 600, marginBottom: '12px' }, children: "Additional Information" }), editingSection !== 'customFields' ? (jsxs(Fragment, { children: [editableCustomFields.map((field) => (jsx("div", { className: "account-field", children: jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: field.label }), jsx("div", { className: "field-value", children: customFieldValues[field.key] !== undefined && customFieldValues[field.key] !== ''
13990
14116
  ? String(customFieldValues[field.key])
13991
- : 'Not set' })] }) }, field.key))), jsx("button", { type: "button", onClick: () => setEditingSection('customFields'), className: "auth-button button-secondary", style: { marginTop: '8px' }, children: "Edit Information" })] })) : (jsxs("form", { onSubmit: handleUpdateCustomFields, className: "edit-form", children: [editableCustomFields.map((field) => (jsx(SchemaFieldRenderer, { field: field, value: customFieldValues[field.key], onChange: handleCustomFieldChange, disabled: loading }, field.key))), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showEmailSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Email Address" }), jsxs("div", { className: "field-value", children: [profile?.email || 'Not set', profile?.emailVerified && (jsx("span", { className: "verification-badge verified", children: "Verified" })), profile?.email && !profile?.emailVerified && (jsx("span", { className: "verification-badge unverified", children: "Unverified" }))] })] }), editingSection !== 'email' && (jsx("button", { type: "button", onClick: () => setEditingSection('email'), className: "auth-button button-secondary", children: "Change Email" }))] }), editingSection === 'email' && (jsxs("form", { onSubmit: handleChangeEmail, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newEmail", children: "New Email" }), jsx("input", { id: "newEmail", type: "email", value: newEmail, onChange: (e) => setNewEmail(e.target.value), placeholder: "new.email@example.com", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "emailPassword", children: "Confirm Password" }), jsx("input", { id: "emailPassword", type: "password", value: emailPassword, onChange: (e) => setEmailPassword(e.target.value), placeholder: "Enter your password", className: "auth-input", required: true })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Email' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPasswordSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Password" }), jsx("div", { className: "field-value", children: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] }), editingSection !== 'password' && (jsx("button", { type: "button", onClick: () => setEditingSection('password'), className: "auth-button button-secondary", children: "Change Password" }))] }), editingSection === 'password' && (jsxs("form", { onSubmit: handleChangePassword, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "currentPassword", children: "Current Password" }), jsx("input", { id: "currentPassword", type: "password", value: currentPassword, onChange: (e) => setCurrentPassword(e.target.value), placeholder: "Enter current password", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPassword", children: "New Password" }), jsx("input", { id: "newPassword", type: "password", value: newPassword, onChange: (e) => setNewPassword(e.target.value), placeholder: "Enter new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "confirmPassword", children: "Confirm New Password" }), jsx("input", { id: "confirmPassword", type: "password", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), placeholder: "Confirm new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Password' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPhoneSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Phone Number" }), jsx("div", { className: "field-value", children: profile?.phoneNumber || 'Not set' })] }), editingSection !== 'phone' && (jsx("button", { type: "button", onClick: () => setEditingSection('phone'), className: "auth-button button-secondary", children: "Change Phone" }))] }), editingSection === 'phone' && (jsxs("form", { onSubmit: handleUpdatePhone, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPhone", children: "New Phone Number" }), jsx("input", { id: "newPhone", type: "tel", value: newPhone, onChange: (e) => setNewPhone(e.target.value), placeholder: "+1234567890", className: "auth-input", required: true })] }), !phoneCodeSent ? (jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleSendPhoneCode, className: "auth-button", disabled: loading || !newPhone, children: loading ? 'Sending...' : 'Send Code' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })) : (jsxs(Fragment, { children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "phoneCode", children: "Verification Code" }), jsx("input", { id: "phoneCode", type: "text", value: phoneCode, onChange: (e) => setPhoneCode(e.target.value), placeholder: "Enter 6-digit code", className: "auth-input", required: true, maxLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Verifying...' : 'Verify & Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] })), showDeleteAccount && (jsxs("section", { className: "account-section danger-zone", children: [jsx("h3", { className: "section-title text-danger", children: "Danger Zone" }), !showDeleteConfirm ? (jsx("button", { type: "button", onClick: () => setShowDeleteConfirm(true), className: "auth-button button-danger", children: "Delete Account" })) : (jsxs("div", { className: "delete-confirm", children: [jsx("p", { className: "warning-text", children: "\u26A0\uFE0F This action cannot be undone. This will permanently delete your account and all associated data." }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deletePassword", children: "Confirm Password" }), jsx("input", { id: "deletePassword", type: "password", value: deletePassword, onChange: (e) => setDeletePassword(e.target.value), placeholder: "Enter your password", className: "auth-input" })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deleteConfirm", children: "Type DELETE to confirm" }), jsx("input", { id: "deleteConfirm", type: "text", value: deleteConfirmText, onChange: (e) => setDeleteConfirmText(e.target.value), placeholder: "DELETE", className: "auth-input" })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleDeleteAccount, className: "auth-button button-danger", disabled: loading, children: loading ? 'Deleting...' : 'Permanently Delete Account' }), jsx("button", { type: "button", onClick: () => {
14117
+ : 'Not set' })] }) }, field.key))), jsx("button", { type: "button", onClick: () => setEditingSection('customFields'), className: "auth-button button-secondary", style: { marginTop: '8px' }, children: "Edit Information" })] })) : (jsxs("form", { onSubmit: handleUpdateCustomFields, className: "edit-form", children: [editableCustomFields.map((field) => (jsx(SchemaFieldRenderer, { field: field, value: customFieldValues[field.key], onChange: handleCustomFieldChange, disabled: loading }, field.key))), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showEmailSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Email Address" }), jsxs("div", { className: "field-value", children: [profile?.email || 'Not set', profile?.emailVerified && (jsx("span", { className: "verification-badge verified", children: "Verified" })), profile?.email && !profile?.emailVerified && (jsx("span", { className: "verification-badge unverified", children: "Unverified" }))] }), isGoogleOnly && (jsx("div", { className: "field-hint", style: { fontSize: '0.8rem', color: 'var(--color-muted-foreground, #888)', marginTop: '4px' }, children: "This email is managed by your Google account" }))] }), editingSection !== 'email' && !isGoogleOnly && !isPhoneOnly && (jsx("button", { type: "button", onClick: () => setEditingSection('email'), className: "auth-button button-secondary", children: "Change Email" }))] }), editingSection === 'email' && !isGoogleOnly && !isPhoneOnly && (jsxs("form", { onSubmit: handleChangeEmail, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newEmail", children: "New Email" }), jsx("input", { id: "newEmail", type: "email", value: newEmail, onChange: (e) => setNewEmail(e.target.value), placeholder: "new.email@example.com", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "emailPassword", children: "Confirm Password" }), jsx("input", { id: "emailPassword", type: "password", value: emailPassword, onChange: (e) => setEmailPassword(e.target.value), placeholder: "Enter your password", className: "auth-input", required: true })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Email' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPasswordSection && !isGoogleOnly && !isPhoneOnly && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Password" }), jsx("div", { className: "field-value", children: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] }), editingSection !== 'password' && (jsx("button", { type: "button", onClick: () => setEditingSection('password'), className: "auth-button button-secondary", children: "Change Password" }))] }), editingSection === 'password' && (jsxs("form", { onSubmit: handleChangePassword, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "currentPassword", children: "Current Password" }), jsx("input", { id: "currentPassword", type: "password", value: currentPassword, onChange: (e) => setCurrentPassword(e.target.value), placeholder: "Enter current password", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPassword", children: "New Password" }), jsx("input", { id: "newPassword", type: "password", value: newPassword, onChange: (e) => setNewPassword(e.target.value), placeholder: "Enter new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "confirmPassword", children: "Confirm New Password" }), jsx("input", { id: "confirmPassword", type: "password", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), placeholder: "Confirm new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Password' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPhoneSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Phone Number" }), jsx("div", { className: "field-value", children: profile?.phoneNumber || 'Not set' })] }), editingSection !== 'phone' && (jsx("button", { type: "button", onClick: () => setEditingSection('phone'), className: "auth-button button-secondary", children: "Change Phone" }))] }), editingSection === 'phone' && (jsxs("form", { onSubmit: handleUpdatePhone, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPhone", children: "New Phone Number" }), jsx("input", { id: "newPhone", type: "tel", value: newPhone, onChange: (e) => setNewPhone(e.target.value), placeholder: "+1234567890", className: "auth-input", required: true })] }), !phoneCodeSent ? (jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleSendPhoneCode, className: "auth-button", disabled: loading || !newPhone, children: loading ? 'Sending...' : 'Send Code' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })) : (jsxs(Fragment, { children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "phoneCode", children: "Verification Code" }), jsx("input", { id: "phoneCode", type: "text", value: phoneCode, onChange: (e) => setPhoneCode(e.target.value), placeholder: "Enter 6-digit code", className: "auth-input", required: true, maxLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Verifying...' : 'Verify & Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] })), showDeleteAccount && (jsxs("section", { className: "account-section danger-zone", children: [jsx("h3", { className: "section-title text-danger", children: "Danger Zone" }), !showDeleteConfirm ? (jsx("button", { type: "button", onClick: () => setShowDeleteConfirm(true), className: "auth-button button-danger", children: "Delete Account" })) : (jsxs("div", { className: "delete-confirm", children: [jsx("p", { className: "warning-text", children: "\u26A0\uFE0F This action cannot be undone. This will permanently delete your account and all associated data." }), hasPassword !== false && (jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deletePassword", children: "Confirm Password" }), jsx("input", { id: "deletePassword", type: "password", value: deletePassword, onChange: (e) => setDeletePassword(e.target.value), placeholder: "Enter your password", className: "auth-input" })] })), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deleteConfirm", children: "Type DELETE to confirm" }), jsx("input", { id: "deleteConfirm", type: "text", value: deleteConfirmText, onChange: (e) => setDeleteConfirmText(e.target.value), placeholder: "DELETE", className: "auth-input" })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleDeleteAccount, className: "auth-button button-danger", disabled: loading, children: loading ? 'Deleting...' : 'Permanently Delete Account' }), jsx("button", { type: "button", onClick: () => {
13992
14118
  setShowDeleteConfirm(false);
13993
14119
  setDeletePassword('');
13994
14120
  setDeleteConfirmText('');