@proveanything/smartlinks-auth-ui 0.1.20 → 0.1.21

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
@@ -11128,6 +11128,7 @@ const TOKEN_KEY = 'token';
11128
11128
  const USER_KEY = 'user';
11129
11129
  const ACCOUNT_DATA_KEY = 'account_data';
11130
11130
  const ACCOUNT_INFO_KEY = 'account_info';
11131
+ const CONTACT_ID_KEY = 'contact_id';
11131
11132
  const ACCOUNT_INFO_TTL = 5 * 60 * 1000; // 5 minutes default
11132
11133
  /**
11133
11134
  * Token Storage Layer
@@ -11178,6 +11179,7 @@ const tokenStorage = {
11178
11179
  await this.clearUser();
11179
11180
  await this.clearAccountData();
11180
11181
  await this.clearAccountInfo();
11182
+ await this.clearContactId();
11181
11183
  },
11182
11184
  async saveAccountData(data) {
11183
11185
  const storage = await getStorage();
@@ -11215,20 +11217,69 @@ const tokenStorage = {
11215
11217
  const storage = await getStorage();
11216
11218
  await storage.removeItem(ACCOUNT_INFO_KEY);
11217
11219
  },
11220
+ async saveContactId(contactId) {
11221
+ const storage = await getStorage();
11222
+ await storage.setItem(CONTACT_ID_KEY, contactId);
11223
+ },
11224
+ async getContactId() {
11225
+ const storage = await getStorage();
11226
+ return await storage.getItem(CONTACT_ID_KEY);
11227
+ },
11228
+ async clearContactId() {
11229
+ const storage = await getStorage();
11230
+ await storage.removeItem(CONTACT_ID_KEY);
11231
+ },
11218
11232
  };
11219
11233
 
11220
11234
  const AuthContext = createContext(undefined);
11221
- const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false }) => {
11235
+ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false,
11236
+ // Contact & Interaction features
11237
+ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, interactionConfig, }) => {
11222
11238
  const [user, setUser] = useState(null);
11223
11239
  const [token, setToken] = useState(null);
11224
11240
  const [accountData, setAccountData] = useState(null);
11225
11241
  const [accountInfo, setAccountInfo] = useState(null);
11226
11242
  const [isLoading, setIsLoading] = useState(true);
11243
+ const [isVerified, setIsVerified] = useState(false);
11244
+ const [isOnline, setIsOnline] = useState(typeof navigator !== 'undefined' ? navigator.onLine : true);
11245
+ // Contact state
11246
+ const [contact, setContact] = useState(null);
11247
+ const [contactId, setContactId] = useState(null);
11227
11248
  const callbacksRef = useRef(new Set());
11228
- // Initialization guard to prevent concurrent runs (NOT persisted across remounts)
11229
11249
  const initializingRef = useRef(false);
11250
+ const pendingVerificationRef = useRef(false);
11251
+ // Default to enabled if collectionId is provided
11252
+ const shouldSyncContacts = enableContactSync ?? !!collectionId;
11253
+ const shouldTrackInteractions = enableInteractionTracking ?? !!collectionId;
11254
+ // Helper to detect if an error is a network error vs auth error
11255
+ const isNetworkError = useCallback((error) => {
11256
+ if (!error)
11257
+ return false;
11258
+ const errorMessage = error?.message?.toLowerCase() || '';
11259
+ const errorName = error?.name?.toLowerCase() || '';
11260
+ if (errorName === 'typeerror' && errorMessage.includes('fetch'))
11261
+ return true;
11262
+ if (errorMessage.includes('network') || errorMessage.includes('offline'))
11263
+ return true;
11264
+ if (errorMessage.includes('failed to fetch'))
11265
+ return true;
11266
+ if (errorMessage.includes('net::err_'))
11267
+ return true;
11268
+ if (errorMessage.includes('timeout'))
11269
+ return true;
11270
+ if (errorMessage.includes('dns'))
11271
+ return true;
11272
+ if (error?.code === 'ENOTFOUND' || error?.code === 'ETIMEDOUT')
11273
+ return true;
11274
+ const status = error?.status || error?.response?.status;
11275
+ if (status === 401 || status === 403)
11276
+ return false;
11277
+ if (typeof navigator !== 'undefined' && !navigator.onLine)
11278
+ return true;
11279
+ return false;
11280
+ }, []);
11230
11281
  // Notify all subscribers of auth state changes
11231
- const notifyAuthStateChange = useCallback((type, currentUser, currentToken, currentAccountData, currentAccountInfo) => {
11282
+ const notifyAuthStateChange = useCallback((type, currentUser, currentToken, currentAccountData, currentAccountInfo, verified, currentContact, currentContactId) => {
11232
11283
  callbacksRef.current.forEach(callback => {
11233
11284
  try {
11234
11285
  callback({
@@ -11236,7 +11287,10 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11236
11287
  user: currentUser,
11237
11288
  token: currentToken,
11238
11289
  accountData: currentAccountData,
11239
- accountInfo: currentAccountInfo
11290
+ accountInfo: currentAccountInfo,
11291
+ isVerified: verified,
11292
+ contact: currentContact,
11293
+ contactId: currentContactId,
11240
11294
  });
11241
11295
  }
11242
11296
  catch (error) {
@@ -11244,9 +11298,128 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11244
11298
  }
11245
11299
  });
11246
11300
  }, []);
11247
- // Initialize auth state - different behavior for proxy mode vs standalone mode
11301
+ // Sync contact to Smartlinks (non-blocking)
11302
+ const syncContact = useCallback(async (authUser, customFields) => {
11303
+ if (!collectionId || !shouldSyncContacts) {
11304
+ console.log('[AuthContext] Contact sync skipped: no collectionId or disabled');
11305
+ return null;
11306
+ }
11307
+ try {
11308
+ console.log('[AuthContext] Syncing contact for user:', authUser.uid);
11309
+ const result = await smartlinks.contact.publicUpsert(collectionId, {
11310
+ userId: authUser.uid,
11311
+ email: authUser.email,
11312
+ displayName: authUser.displayName,
11313
+ phone: authUser.phoneNumber,
11314
+ customFields: customFields || {},
11315
+ source: 'authkit',
11316
+ });
11317
+ console.log('[AuthContext] Contact synced:', result.contactId);
11318
+ // Store contact ID locally
11319
+ if (!proxyMode) {
11320
+ await tokenStorage.saveContactId(result.contactId);
11321
+ }
11322
+ setContactId(result.contactId);
11323
+ // Fetch full contact to get customFields
11324
+ try {
11325
+ const fullContact = await smartlinks.contact.lookup(collectionId, {
11326
+ email: authUser.email
11327
+ });
11328
+ setContact(fullContact);
11329
+ notifyAuthStateChange('CONTACT_SYNCED', authUser, token, accountData, accountInfo, isVerified, fullContact, result.contactId);
11330
+ }
11331
+ catch (lookupErr) {
11332
+ console.warn('[AuthContext] Failed to lookup full contact:', lookupErr);
11333
+ }
11334
+ return result.contactId;
11335
+ }
11336
+ catch (err) {
11337
+ console.warn('[AuthContext] Contact sync failed (non-blocking):', err);
11338
+ return null;
11339
+ }
11340
+ }, [collectionId, shouldSyncContacts, proxyMode, token, accountData, accountInfo, isVerified, notifyAuthStateChange]);
11341
+ // Track interaction event (non-blocking)
11342
+ const trackInteraction = useCallback(async (eventType, userId, currentContactId, metadata) => {
11343
+ if (!collectionId || !shouldTrackInteractions) {
11344
+ console.log('[AuthContext] Interaction tracking skipped: no collectionId or disabled');
11345
+ return;
11346
+ }
11347
+ const interactionIdMap = {
11348
+ login: interactionConfig?.login || 'authkit.login',
11349
+ logout: interactionConfig?.logout || 'authkit.logout',
11350
+ signup: interactionConfig?.signup || 'authkit.signup',
11351
+ session_restore: interactionConfig?.sessionRestore,
11352
+ };
11353
+ const interactionId = interactionIdMap[eventType];
11354
+ if (!interactionId) {
11355
+ console.log(`[AuthContext] No interaction ID for ${eventType}, skipping`);
11356
+ return;
11357
+ }
11358
+ try {
11359
+ console.log(`[AuthContext] Tracking interaction: ${interactionId}`);
11360
+ await smartlinks.interactions.submitPublicEvent(collectionId, {
11361
+ collectionId,
11362
+ interactionId,
11363
+ userId,
11364
+ contactId: currentContactId || undefined,
11365
+ appId: interactionAppId,
11366
+ eventType,
11367
+ outcome: 'completed',
11368
+ metadata: {
11369
+ ...metadata,
11370
+ timestamp: new Date().toISOString(),
11371
+ source: 'authkit',
11372
+ },
11373
+ });
11374
+ console.log(`[AuthContext] Tracked interaction: ${interactionId}`);
11375
+ notifyAuthStateChange('INTERACTION_TRACKED', user, token, accountData, accountInfo, isVerified, contact, contactId);
11376
+ }
11377
+ catch (err) {
11378
+ console.warn('[AuthContext] Interaction tracking failed (non-blocking):', err);
11379
+ }
11380
+ }, [collectionId, shouldTrackInteractions, interactionAppId, interactionConfig, user, token, accountData, accountInfo, isVerified, contact, contactId, notifyAuthStateChange]);
11381
+ // Get contact (with optional refresh)
11382
+ const getContact = useCallback(async (forceRefresh = false) => {
11383
+ if (!collectionId) {
11384
+ console.log('[AuthContext] getContact: no collectionId');
11385
+ return null;
11386
+ }
11387
+ // Need either email or userId to lookup contact
11388
+ if (!user?.email && !user?.uid) {
11389
+ console.log('[AuthContext] getContact: no user email or uid');
11390
+ return null;
11391
+ }
11392
+ if (contact && !forceRefresh) {
11393
+ return contact;
11394
+ }
11395
+ try {
11396
+ // Prefer email lookup, fallback to userId for phone-only users
11397
+ const lookupParams = user.email
11398
+ ? { email: user.email }
11399
+ : { userId: user.uid };
11400
+ console.log('[AuthContext] Fetching contact with:', lookupParams);
11401
+ const result = await smartlinks.contact.lookup(collectionId, lookupParams);
11402
+ setContact(result);
11403
+ setContactId(result.contactId);
11404
+ return result;
11405
+ }
11406
+ catch (err) {
11407
+ console.warn('[AuthContext] Failed to get contact:', err);
11408
+ return null;
11409
+ }
11410
+ }, [collectionId, user, contact]);
11411
+ // Update contact custom fields
11412
+ const updateContactCustomFields = useCallback(async (customFields) => {
11413
+ if (!collectionId || !contactId) {
11414
+ throw new Error('No contact to update. Ensure collectionId is provided and user is synced.');
11415
+ }
11416
+ console.log('[AuthContext] Updating contact custom fields:', contactId);
11417
+ const updated = await smartlinks.contact.update(collectionId, contactId, { customFields });
11418
+ setContact(updated);
11419
+ return updated;
11420
+ }, [collectionId, contactId]);
11421
+ // Initialize auth state
11248
11422
  useEffect(() => {
11249
- // Prevent concurrent initialization only
11250
11423
  if (initializingRef.current) {
11251
11424
  console.log('[AuthContext] Skipping initialization - already in progress');
11252
11425
  return;
@@ -11260,12 +11433,9 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11260
11433
  console.log('[AuthContext] Proxy mode: checking for existing session via auth.getAccount()');
11261
11434
  try {
11262
11435
  const accountResponse = await smartlinks.auth.getAccount();
11263
- // auth.getAccount() always returns a response object, but uid will be
11264
- // empty/undefined if no user is logged in
11265
11436
  const accountAny = accountResponse;
11266
11437
  const hasValidSession = accountAny?.uid && accountAny.uid.length > 0;
11267
11438
  if (hasValidSession && isMounted) {
11268
- // User is logged in with valid account
11269
11439
  const userFromAccount = {
11270
11440
  uid: accountAny.uid,
11271
11441
  email: accountAny?.email,
@@ -11275,8 +11445,11 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11275
11445
  setUser(userFromAccount);
11276
11446
  setAccountData(accountResponse);
11277
11447
  setAccountInfo(accountResponse);
11448
+ setIsVerified(true);
11278
11449
  console.log('[AuthContext] Proxy mode: initialized from parent account, uid:', accountAny.uid);
11279
- notifyAuthStateChange('LOGIN', userFromAccount, null, accountResponse, accountResponse);
11450
+ notifyAuthStateChange('LOGIN', userFromAccount, null, accountResponse, accountResponse, true);
11451
+ // Sync contact in background (proxy mode)
11452
+ syncContact(userFromAccount, accountResponse);
11280
11453
  }
11281
11454
  else if (isMounted) {
11282
11455
  console.log('[AuthContext] Proxy mode: no valid session (no uid), awaiting login');
@@ -11291,29 +11464,56 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11291
11464
  }
11292
11465
  return;
11293
11466
  }
11294
- // STANDALONE MODE: Load from persistent storage
11467
+ // STANDALONE MODE: Optimistic restoration with background verification
11295
11468
  const storedToken = await tokenStorage.getToken();
11296
11469
  const storedUser = await tokenStorage.getUser();
11297
11470
  const storedAccountData = await tokenStorage.getAccountData();
11471
+ const storedContactId = await tokenStorage.getContactId();
11298
11472
  if (storedToken && storedUser) {
11299
- // Verify token FIRST before setting state
11473
+ if (isMounted) {
11474
+ setToken(storedToken.token);
11475
+ setUser(storedUser);
11476
+ setAccountData(storedAccountData);
11477
+ if (storedContactId) {
11478
+ setContactId(storedContactId);
11479
+ }
11480
+ console.log('[AuthContext] Session restored optimistically (pending verification)');
11481
+ notifyAuthStateChange('SESSION_RESTORED_OFFLINE', storedUser, storedToken.token, storedAccountData || null, null, false, null, storedContactId);
11482
+ }
11483
+ // BACKGROUND: Verify token
11300
11484
  try {
11301
- console.log('[AuthContext] Verifying stored token...');
11485
+ console.log('[AuthContext] Verifying stored token in background...');
11302
11486
  await smartlinks.auth.verifyToken(storedToken.token);
11303
- // Only set state if verification succeeded and component still mounted
11304
11487
  if (isMounted) {
11305
- setToken(storedToken.token);
11306
- setUser(storedUser);
11307
- setAccountData(storedAccountData);
11308
- console.log('[AuthContext] Session restored successfully');
11309
- // Notify subscribers that session was restored (critical for app to know we're logged in)
11310
- notifyAuthStateChange('LOGIN', storedUser, storedToken.token, storedAccountData || null);
11488
+ setIsVerified(true);
11489
+ pendingVerificationRef.current = false;
11490
+ console.log('[AuthContext] Session verified successfully');
11491
+ notifyAuthStateChange('SESSION_VERIFIED', storedUser, storedToken.token, storedAccountData || null, null, true, null, storedContactId);
11492
+ // Track session restore interaction (optional)
11493
+ if (interactionConfig?.sessionRestore) {
11494
+ trackInteraction('session_restore', storedUser.uid, storedContactId);
11495
+ }
11311
11496
  }
11312
11497
  }
11313
11498
  catch (err) {
11314
- console.warn('[AuthContext] Token verification failed, clearing stored credentials:', err);
11315
- await tokenStorage.clearAll();
11316
- // Don't set user state - leave as logged out
11499
+ if (isNetworkError(err)) {
11500
+ console.warn('[AuthContext] Network error during verification, will retry on reconnect:', err);
11501
+ pendingVerificationRef.current = true;
11502
+ }
11503
+ else {
11504
+ console.warn('[AuthContext] Token verification failed (auth error), clearing credentials:', err);
11505
+ if (isMounted) {
11506
+ setToken(null);
11507
+ setUser(null);
11508
+ setAccountData(null);
11509
+ setContactId(null);
11510
+ setContact(null);
11511
+ setIsVerified(false);
11512
+ pendingVerificationRef.current = false;
11513
+ }
11514
+ await tokenStorage.clearAll();
11515
+ notifyAuthStateChange('LOGOUT', null, null, null, null, false);
11516
+ }
11317
11517
  }
11318
11518
  }
11319
11519
  // Load cached account info if available
@@ -11335,18 +11535,16 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11335
11535
  }
11336
11536
  };
11337
11537
  initializeAuth();
11338
- // Cleanup for hot reload
11339
11538
  return () => {
11340
11539
  isMounted = false;
11341
11540
  };
11342
- }, [proxyMode, notifyAuthStateChange]);
11541
+ }, [proxyMode, notifyAuthStateChange, isNetworkError, syncContact, trackInteraction, interactionConfig?.sessionRestore]);
11343
11542
  // Listen for parent auth state changes (proxy mode only)
11344
11543
  useEffect(() => {
11345
11544
  if (!proxyMode)
11346
11545
  return;
11347
11546
  console.log('[AuthContext] Proxy mode: setting up parent message listener');
11348
11547
  const handleParentMessage = (event) => {
11349
- // Handle auth state pushed from parent
11350
11548
  if (event.data?.type === 'smartlinks:authkit:state') {
11351
11549
  const { user: parentUser, accountData: parentAccountData, authenticated } = event.data.payload || {};
11352
11550
  console.log('[AuthContext] Proxy mode: received state from parent:', { authenticated });
@@ -11360,15 +11558,20 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11360
11558
  setUser(userObj);
11361
11559
  setAccountData(parentAccountData || null);
11362
11560
  setAccountInfo(parentAccountData || null);
11363
- notifyAuthStateChange('CROSS_TAB_SYNC', userObj, null, parentAccountData || null, parentAccountData || null);
11561
+ setIsVerified(true);
11562
+ notifyAuthStateChange('CROSS_TAB_SYNC', userObj, null, parentAccountData || null, parentAccountData || null, true);
11563
+ // Sync contact on cross-tab state
11564
+ syncContact(userObj, parentAccountData);
11364
11565
  }
11365
11566
  else {
11366
- // Parent indicates no session / logged out
11367
11567
  setUser(null);
11368
11568
  setToken(null);
11369
11569
  setAccountData(null);
11370
11570
  setAccountInfo(null);
11371
- notifyAuthStateChange('LOGOUT', null, null, null, null);
11571
+ setContact(null);
11572
+ setContactId(null);
11573
+ setIsVerified(false);
11574
+ notifyAuthStateChange('LOGOUT', null, null, null, null, false);
11372
11575
  }
11373
11576
  }
11374
11577
  };
@@ -11377,54 +11580,60 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11377
11580
  console.log('[AuthContext] Proxy mode: cleaning up parent message listener');
11378
11581
  window.removeEventListener('message', handleParentMessage);
11379
11582
  };
11380
- }, [proxyMode, notifyAuthStateChange]);
11583
+ }, [proxyMode, notifyAuthStateChange, syncContact]);
11381
11584
  // Cross-tab synchronization - standalone mode only
11382
11585
  useEffect(() => {
11383
11586
  if (proxyMode)
11384
- return; // Skip cross-tab sync in proxy mode
11587
+ return;
11385
11588
  console.log('[AuthContext] Setting up cross-tab synchronization');
11386
11589
  const unsubscribe = onStorageChange(async (event) => {
11387
11590
  console.log('[AuthContext] Cross-tab storage event:', event.type, event.key);
11388
11591
  try {
11389
11592
  if (event.type === 'clear') {
11390
- // Another tab cleared all storage (logout)
11391
11593
  console.log('[AuthContext] Detected logout in another tab');
11392
11594
  setToken(null);
11393
11595
  setUser(null);
11394
11596
  setAccountData(null);
11395
11597
  setAccountInfo(null);
11598
+ setContact(null);
11599
+ setContactId(null);
11600
+ setIsVerified(false);
11396
11601
  smartlinks.auth.logout();
11397
- notifyAuthStateChange('CROSS_TAB_SYNC', null, null, null);
11602
+ notifyAuthStateChange('CROSS_TAB_SYNC', null, null, null, null, false);
11398
11603
  }
11399
11604
  else if (event.type === 'remove' && (event.key === 'token' || event.key === 'user')) {
11400
- // Another tab removed token or user (logout)
11401
11605
  console.log('[AuthContext] Detected token/user removal in another tab');
11402
11606
  setToken(null);
11403
11607
  setUser(null);
11404
11608
  setAccountData(null);
11405
11609
  setAccountInfo(null);
11610
+ setContact(null);
11611
+ setContactId(null);
11612
+ setIsVerified(false);
11406
11613
  smartlinks.auth.logout();
11407
- notifyAuthStateChange('CROSS_TAB_SYNC', null, null, null);
11614
+ notifyAuthStateChange('CROSS_TAB_SYNC', null, null, null, null, false);
11408
11615
  }
11409
11616
  else if (event.type === 'set' && event.key === 'token') {
11410
- // Another tab set a new token (login)
11411
11617
  console.log('[AuthContext] Detected login in another tab');
11412
11618
  const storedToken = await tokenStorage.getToken();
11413
11619
  const storedUser = await tokenStorage.getUser();
11414
11620
  const storedAccountData = await tokenStorage.getAccountData();
11621
+ const storedContactId = await tokenStorage.getContactId();
11415
11622
  if (storedToken && storedUser) {
11416
11623
  setToken(storedToken.token);
11417
11624
  setUser(storedUser);
11418
11625
  setAccountData(storedAccountData);
11419
- // Set bearer token in global Smartlinks SDK
11626
+ if (storedContactId) {
11627
+ setContactId(storedContactId);
11628
+ }
11629
+ setIsVerified(true);
11420
11630
  smartlinks.auth.verifyToken(storedToken.token).catch(err => {
11421
11631
  console.warn('[AuthContext] Failed to restore bearer token from cross-tab sync:', err);
11422
11632
  });
11423
- notifyAuthStateChange('CROSS_TAB_SYNC', storedUser, storedToken.token, storedAccountData);
11633
+ notifyAuthStateChange('CROSS_TAB_SYNC', storedUser, storedToken.token, storedAccountData, null, true, null, storedContactId);
11424
11634
  }
11425
11635
  }
11426
11636
  else if (event.type === 'set' && event.key === 'account_info') {
11427
- // Another tab fetched fresh account info
11428
11637
  const cached = await tokenStorage.getAccountInfo();
11429
11638
  if (cached && !cached.isStale) {
11430
11639
  setAccountInfo(cached.data);
@@ -11441,7 +11650,7 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11441
11650
  unsubscribe();
11442
11651
  };
11443
11652
  }, [proxyMode, notifyAuthStateChange]);
11444
- const login = useCallback(async (authToken, authUser, authAccountData) => {
11653
+ const login = useCallback(async (authToken, authUser, authAccountData, isNewUser) => {
11445
11654
  try {
11446
11655
  // Only persist to storage in standalone mode
11447
11656
  if (!proxyMode) {
@@ -11450,7 +11659,6 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11450
11659
  if (authAccountData) {
11451
11660
  await tokenStorage.saveAccountData(authAccountData);
11452
11661
  }
11453
- // Set bearer token in global Smartlinks SDK via auth.verifyToken
11454
11662
  smartlinks.auth.verifyToken(authToken).catch(err => {
11455
11663
  console.warn('Failed to set bearer token on login:', err);
11456
11664
  });
@@ -11459,8 +11667,9 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11459
11667
  setToken(authToken);
11460
11668
  setUser(authUser);
11461
11669
  setAccountData(authAccountData || null);
11670
+ setIsVerified(true);
11671
+ pendingVerificationRef.current = false;
11462
11672
  // Cross-iframe auth state synchronization
11463
- // Always notify parent frame of login (both modes, but especially important in proxy mode)
11464
11673
  if (iframe.isIframe()) {
11465
11674
  console.log('[AuthContext] Notifying parent of login via postMessage');
11466
11675
  iframe.sendParentCustom('smartlinks:authkit:login', {
@@ -11469,7 +11678,13 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11469
11678
  accountData: authAccountData || null
11470
11679
  });
11471
11680
  }
11472
- notifyAuthStateChange('LOGIN', authUser, authToken, authAccountData || null);
11681
+ notifyAuthStateChange('LOGIN', authUser, authToken, authAccountData || null, null, true);
11682
+ // Sync contact (non-blocking)
11683
+ const newContactId = await syncContact(authUser, authAccountData);
11684
+ // Track interaction (non-blocking)
11685
+ trackInteraction(isNewUser ? 'signup' : 'login', authUser.uid, newContactId, {
11686
+ provider: authUser.email ? 'email' : 'phone',
11687
+ });
11473
11688
  // Optionally preload account info on login (standalone mode only)
11474
11689
  if (!proxyMode && preloadAccountInfo) {
11475
11690
  getAccount(true).catch(error => {
@@ -11481,8 +11696,10 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11481
11696
  console.error('Failed to save auth data to storage:', error);
11482
11697
  throw error;
11483
11698
  }
11484
- }, [proxyMode, notifyAuthStateChange, preloadAccountInfo]);
11699
+ }, [proxyMode, notifyAuthStateChange, preloadAccountInfo, syncContact, trackInteraction]);
11485
11700
  const logout = useCallback(async () => {
11701
+ const currentUser = user;
11702
+ const currentContactId = contactId;
11486
11703
  try {
11487
11704
  // Only clear persistent storage in standalone mode
11488
11705
  if (!proxyMode) {
@@ -11494,21 +11711,27 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11494
11711
  setUser(null);
11495
11712
  setAccountData(null);
11496
11713
  setAccountInfo(null);
11714
+ setContact(null);
11715
+ setContactId(null);
11716
+ setIsVerified(false);
11717
+ pendingVerificationRef.current = false;
11497
11718
  // Cross-iframe auth state synchronization
11498
- // Always notify parent frame of logout
11499
11719
  if (iframe.isIframe()) {
11500
11720
  console.log('[AuthContext] Notifying parent of logout via postMessage');
11501
11721
  iframe.sendParentCustom('smartlinks:authkit:logout', {});
11502
11722
  }
11503
- notifyAuthStateChange('LOGOUT', null, null, null);
11723
+ notifyAuthStateChange('LOGOUT', null, null, null, null, false);
11724
+ // Track logout interaction (fire and forget)
11725
+ if (currentUser && collectionId && shouldTrackInteractions) {
11726
+ trackInteraction('logout', currentUser.uid, currentContactId);
11727
+ }
11504
11728
  }
11505
11729
  catch (error) {
11506
11730
  console.error('Failed to clear auth data from storage:', error);
11507
11731
  }
11508
- }, [proxyMode, notifyAuthStateChange]);
11732
+ }, [proxyMode, notifyAuthStateChange, user, contactId, collectionId, shouldTrackInteractions, trackInteraction]);
11509
11733
  const getToken = useCallback(async () => {
11510
11734
  if (proxyMode) {
11511
- // In proxy mode, token is managed by parent - return memory state
11512
11735
  return token;
11513
11736
  }
11514
11737
  const storedToken = await tokenStorage.getToken();
@@ -11517,23 +11740,19 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11517
11740
  const refreshToken = useCallback(async () => {
11518
11741
  throw new Error('Token refresh must be implemented via your backend API');
11519
11742
  }, []);
11520
- // Get account with intelligent caching (or direct parent fetch in proxy mode)
11521
11743
  const getAccount = useCallback(async (forceRefresh = false) => {
11522
11744
  try {
11523
11745
  if (proxyMode) {
11524
- // PROXY MODE: Always fetch from parent via proxied API, no local cache
11525
11746
  console.log('[AuthContext] Proxy mode: fetching account from parent');
11526
11747
  const freshAccountInfo = await smartlinks.auth.getAccount();
11527
11748
  setAccountInfo(freshAccountInfo);
11528
11749
  setAccountData(freshAccountInfo);
11529
- notifyAuthStateChange('ACCOUNT_REFRESH', user, token, freshAccountInfo, freshAccountInfo);
11750
+ notifyAuthStateChange('ACCOUNT_REFRESH', user, token, freshAccountInfo, freshAccountInfo, isVerified, contact, contactId);
11530
11751
  return freshAccountInfo;
11531
11752
  }
11532
- // STANDALONE MODE: Use caching
11533
11753
  if (!token) {
11534
11754
  throw new Error('Not authenticated. Please login first.');
11535
11755
  }
11536
- // Check cache unless force refresh
11537
11756
  if (!forceRefresh) {
11538
11757
  const cached = await tokenStorage.getAccountInfo();
11539
11758
  if (cached && !cached.isStale) {
@@ -11541,18 +11760,15 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11541
11760
  return cached.data;
11542
11761
  }
11543
11762
  }
11544
- // Fetch fresh data from API
11545
11763
  console.log('[AuthContext] Fetching fresh account info from API');
11546
11764
  const freshAccountInfo = await smartlinks.auth.getAccount();
11547
- // Cache the fresh data
11548
11765
  await tokenStorage.saveAccountInfo(freshAccountInfo, accountCacheTTL);
11549
11766
  setAccountInfo(freshAccountInfo);
11550
- notifyAuthStateChange('ACCOUNT_REFRESH', user, token, accountData, freshAccountInfo);
11767
+ notifyAuthStateChange('ACCOUNT_REFRESH', user, token, accountData, freshAccountInfo, isVerified, contact, contactId);
11551
11768
  return freshAccountInfo;
11552
11769
  }
11553
11770
  catch (error) {
11554
11771
  console.error('[AuthContext] Failed to get account info:', error);
11555
- // Fallback to stale cache if API fails (standalone mode only)
11556
11772
  if (!proxyMode) {
11557
11773
  const cached = await tokenStorage.getAccountInfo();
11558
11774
  if (cached) {
@@ -11562,12 +11778,10 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11562
11778
  }
11563
11779
  throw error;
11564
11780
  }
11565
- }, [proxyMode, token, accountCacheTTL, user, accountData, notifyAuthStateChange]);
11566
- // Convenience method for explicit refresh
11781
+ }, [proxyMode, token, accountCacheTTL, user, accountData, isVerified, contact, contactId, notifyAuthStateChange]);
11567
11782
  const refreshAccount = useCallback(async () => {
11568
11783
  return await getAccount(true);
11569
11784
  }, [getAccount]);
11570
- // Clear account cache (no-op in proxy mode)
11571
11785
  const clearAccountCache = useCallback(async () => {
11572
11786
  if (!proxyMode) {
11573
11787
  await tokenStorage.clearAccountInfo();
@@ -11576,19 +11790,77 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11576
11790
  }, [proxyMode]);
11577
11791
  const onAuthStateChange = useCallback((callback) => {
11578
11792
  callbacksRef.current.add(callback);
11579
- // Return unsubscribe function
11580
11793
  return () => {
11581
11794
  callbacksRef.current.delete(callback);
11582
11795
  };
11583
11796
  }, []);
11797
+ const retryVerification = useCallback(async () => {
11798
+ if (!token || !user) {
11799
+ console.log('[AuthContext] No session to verify');
11800
+ return false;
11801
+ }
11802
+ if (isVerified) {
11803
+ console.log('[AuthContext] Session already verified');
11804
+ return true;
11805
+ }
11806
+ try {
11807
+ console.log('[AuthContext] Retrying session verification...');
11808
+ await smartlinks.auth.verifyToken(token);
11809
+ setIsVerified(true);
11810
+ pendingVerificationRef.current = false;
11811
+ console.log('[AuthContext] Session verified on retry');
11812
+ notifyAuthStateChange('SESSION_VERIFIED', user, token, accountData, accountInfo, true, contact, contactId);
11813
+ return true;
11814
+ }
11815
+ catch (err) {
11816
+ if (isNetworkError(err)) {
11817
+ console.warn('[AuthContext] Network still unavailable, will retry later');
11818
+ return false;
11819
+ }
11820
+ else {
11821
+ console.warn('[AuthContext] Session invalid on retry, logging out');
11822
+ await logout();
11823
+ return false;
11824
+ }
11825
+ }
11826
+ }, [token, user, isVerified, accountData, accountInfo, contact, contactId, notifyAuthStateChange, isNetworkError, logout]);
11827
+ // Online/offline event listener for auto-retry verification
11828
+ useEffect(() => {
11829
+ if (proxyMode)
11830
+ return;
11831
+ const handleOnline = () => {
11832
+ console.log('[AuthContext] Network reconnected');
11833
+ setIsOnline(true);
11834
+ if (pendingVerificationRef.current && token && user) {
11835
+ console.log('[AuthContext] Retrying pending verification after reconnect...');
11836
+ retryVerification();
11837
+ }
11838
+ };
11839
+ const handleOffline = () => {
11840
+ console.log('[AuthContext] Network disconnected');
11841
+ setIsOnline(false);
11842
+ };
11843
+ window.addEventListener('online', handleOnline);
11844
+ window.addEventListener('offline', handleOffline);
11845
+ return () => {
11846
+ window.removeEventListener('online', handleOnline);
11847
+ window.removeEventListener('offline', handleOffline);
11848
+ };
11849
+ }, [proxyMode, token, user, retryVerification]);
11584
11850
  const value = {
11585
11851
  user,
11586
11852
  token,
11587
11853
  accountData,
11588
11854
  accountInfo,
11589
11855
  isAuthenticated: !!user,
11856
+ isVerified,
11590
11857
  isLoading,
11858
+ isOnline,
11591
11859
  proxyMode,
11860
+ contact,
11861
+ contactId,
11862
+ getContact,
11863
+ updateContactCustomFields,
11592
11864
  login,
11593
11865
  logout,
11594
11866
  getToken,
@@ -11597,6 +11869,7 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11597
11869
  refreshAccount,
11598
11870
  clearAccountCache,
11599
11871
  onAuthStateChange,
11872
+ retryVerification,
11600
11873
  };
11601
11874
  return jsx(AuthContext.Provider, { value: value, children: children });
11602
11875
  };
@@ -11875,7 +12148,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11875
12148
  const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
11876
12149
  if ((verificationMode === 'verify-then-auto-login' || verificationMode === 'immediate') && response.token) {
11877
12150
  // Auto-login modes: Log the user in immediately if token is provided
11878
- auth.login(response.token, response.user, response.accountData);
12151
+ auth.login(response.token, response.user, response.accountData, true);
11879
12152
  setAuthSuccess(true);
11880
12153
  setSuccessMessage('Email verified successfully! You are now logged in.');
11881
12154
  onAuthSuccess(response.token, response.user, response.accountData);
@@ -11918,7 +12191,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11918
12191
  const response = await api.verifyMagicLink(token);
11919
12192
  // Auto-login with magic link if token is provided
11920
12193
  if (response.token) {
11921
- auth.login(response.token, response.user, response.accountData);
12194
+ auth.login(response.token, response.user, response.accountData, true);
11922
12195
  setAuthSuccess(true);
11923
12196
  setSuccessMessage('Magic link verified! You are now logged in.');
11924
12197
  onAuthSuccess(response.token, response.user, response.accountData);
@@ -11993,8 +12266,8 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11993
12266
  if (mode === 'register') {
11994
12267
  // Handle different verification modes
11995
12268
  if (verificationMode === 'immediate' && response.token) {
11996
- // Immediate mode: Log in right away if token is provided
11997
- auth.login(response.token, response.user, response.accountData);
12269
+ // Immediate mode: Log in right away if token is provided (isNewUser=true for registration)
12270
+ auth.login(response.token, response.user, response.accountData, true);
11998
12271
  setAuthSuccess(true);
11999
12272
  const deadline = response.emailVerificationDeadline
12000
12273
  ? new Date(response.emailVerificationDeadline).toLocaleString()
@@ -12030,7 +12303,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12030
12303
  if (response.requiresEmailVerification) {
12031
12304
  throw new Error('Please verify your email before logging in. Check your inbox for the verification link.');
12032
12305
  }
12033
- auth.login(response.token, response.user, response.accountData);
12306
+ auth.login(response.token, response.user, response.accountData, false);
12034
12307
  setAuthSuccess(true);
12035
12308
  setSuccessMessage('Login successful!');
12036
12309
  onAuthSuccess(response.token, response.user, response.accountData);
@@ -12190,7 +12463,8 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12190
12463
  tokenType: 'access_token',
12191
12464
  });
12192
12465
  if (authResponse.token) {
12193
- auth.login(authResponse.token, authResponse.user, authResponse.accountData);
12466
+ // Google OAuth can be login or signup - use isNewUser flag from backend if available
12467
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser);
12194
12468
  setAuthSuccess(true);
12195
12469
  setSuccessMessage('Google login successful!');
12196
12470
  onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
@@ -12239,7 +12513,8 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12239
12513
  const idToken = response.credential;
12240
12514
  const authResponse = await api.loginWithGoogle(idToken);
12241
12515
  if (authResponse.token) {
12242
- auth.login(authResponse.token, authResponse.user, authResponse.accountData);
12516
+ // Google OAuth can be login or signup - use isNewUser flag from backend if available
12517
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser);
12243
12518
  setAuthSuccess(true);
12244
12519
  setSuccessMessage('Google login successful!');
12245
12520
  onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
@@ -12300,7 +12575,8 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12300
12575
  const response = await api.verifyPhoneCode(phoneNumber, verificationCode);
12301
12576
  // Update auth context with account data if token is provided
12302
12577
  if (response.token) {
12303
- auth.login(response.token, response.user, response.accountData);
12578
+ // Phone auth can be login or signup - use isNewUser flag from backend if available
12579
+ auth.login(response.token, response.user, response.accountData, response.isNewUser);
12304
12580
  onAuthSuccess(response.token, response.user, response.accountData);
12305
12581
  if (redirectUrl) {
12306
12582
  window.location.href = redirectUrl;