@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.js CHANGED
@@ -11148,6 +11148,7 @@ const TOKEN_KEY = 'token';
11148
11148
  const USER_KEY = 'user';
11149
11149
  const ACCOUNT_DATA_KEY = 'account_data';
11150
11150
  const ACCOUNT_INFO_KEY = 'account_info';
11151
+ const CONTACT_ID_KEY = 'contact_id';
11151
11152
  const ACCOUNT_INFO_TTL = 5 * 60 * 1000; // 5 minutes default
11152
11153
  /**
11153
11154
  * Token Storage Layer
@@ -11198,6 +11199,7 @@ const tokenStorage = {
11198
11199
  await this.clearUser();
11199
11200
  await this.clearAccountData();
11200
11201
  await this.clearAccountInfo();
11202
+ await this.clearContactId();
11201
11203
  },
11202
11204
  async saveAccountData(data) {
11203
11205
  const storage = await getStorage();
@@ -11235,20 +11237,69 @@ const tokenStorage = {
11235
11237
  const storage = await getStorage();
11236
11238
  await storage.removeItem(ACCOUNT_INFO_KEY);
11237
11239
  },
11240
+ async saveContactId(contactId) {
11241
+ const storage = await getStorage();
11242
+ await storage.setItem(CONTACT_ID_KEY, contactId);
11243
+ },
11244
+ async getContactId() {
11245
+ const storage = await getStorage();
11246
+ return await storage.getItem(CONTACT_ID_KEY);
11247
+ },
11248
+ async clearContactId() {
11249
+ const storage = await getStorage();
11250
+ await storage.removeItem(CONTACT_ID_KEY);
11251
+ },
11238
11252
  };
11239
11253
 
11240
11254
  const AuthContext = React.createContext(undefined);
11241
- const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false }) => {
11255
+ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false,
11256
+ // Contact & Interaction features
11257
+ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, interactionConfig, }) => {
11242
11258
  const [user, setUser] = React.useState(null);
11243
11259
  const [token, setToken] = React.useState(null);
11244
11260
  const [accountData, setAccountData] = React.useState(null);
11245
11261
  const [accountInfo, setAccountInfo] = React.useState(null);
11246
11262
  const [isLoading, setIsLoading] = React.useState(true);
11263
+ const [isVerified, setIsVerified] = React.useState(false);
11264
+ const [isOnline, setIsOnline] = React.useState(typeof navigator !== 'undefined' ? navigator.onLine : true);
11265
+ // Contact state
11266
+ const [contact, setContact] = React.useState(null);
11267
+ const [contactId, setContactId] = React.useState(null);
11247
11268
  const callbacksRef = React.useRef(new Set());
11248
- // Initialization guard to prevent concurrent runs (NOT persisted across remounts)
11249
11269
  const initializingRef = React.useRef(false);
11270
+ const pendingVerificationRef = React.useRef(false);
11271
+ // Default to enabled if collectionId is provided
11272
+ const shouldSyncContacts = enableContactSync ?? !!collectionId;
11273
+ const shouldTrackInteractions = enableInteractionTracking ?? !!collectionId;
11274
+ // Helper to detect if an error is a network error vs auth error
11275
+ const isNetworkError = React.useCallback((error) => {
11276
+ if (!error)
11277
+ return false;
11278
+ const errorMessage = error?.message?.toLowerCase() || '';
11279
+ const errorName = error?.name?.toLowerCase() || '';
11280
+ if (errorName === 'typeerror' && errorMessage.includes('fetch'))
11281
+ return true;
11282
+ if (errorMessage.includes('network') || errorMessage.includes('offline'))
11283
+ return true;
11284
+ if (errorMessage.includes('failed to fetch'))
11285
+ return true;
11286
+ if (errorMessage.includes('net::err_'))
11287
+ return true;
11288
+ if (errorMessage.includes('timeout'))
11289
+ return true;
11290
+ if (errorMessage.includes('dns'))
11291
+ return true;
11292
+ if (error?.code === 'ENOTFOUND' || error?.code === 'ETIMEDOUT')
11293
+ return true;
11294
+ const status = error?.status || error?.response?.status;
11295
+ if (status === 401 || status === 403)
11296
+ return false;
11297
+ if (typeof navigator !== 'undefined' && !navigator.onLine)
11298
+ return true;
11299
+ return false;
11300
+ }, []);
11250
11301
  // Notify all subscribers of auth state changes
11251
- const notifyAuthStateChange = React.useCallback((type, currentUser, currentToken, currentAccountData, currentAccountInfo) => {
11302
+ const notifyAuthStateChange = React.useCallback((type, currentUser, currentToken, currentAccountData, currentAccountInfo, verified, currentContact, currentContactId) => {
11252
11303
  callbacksRef.current.forEach(callback => {
11253
11304
  try {
11254
11305
  callback({
@@ -11256,7 +11307,10 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11256
11307
  user: currentUser,
11257
11308
  token: currentToken,
11258
11309
  accountData: currentAccountData,
11259
- accountInfo: currentAccountInfo
11310
+ accountInfo: currentAccountInfo,
11311
+ isVerified: verified,
11312
+ contact: currentContact,
11313
+ contactId: currentContactId,
11260
11314
  });
11261
11315
  }
11262
11316
  catch (error) {
@@ -11264,9 +11318,128 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11264
11318
  }
11265
11319
  });
11266
11320
  }, []);
11267
- // Initialize auth state - different behavior for proxy mode vs standalone mode
11321
+ // Sync contact to Smartlinks (non-blocking)
11322
+ const syncContact = React.useCallback(async (authUser, customFields) => {
11323
+ if (!collectionId || !shouldSyncContacts) {
11324
+ console.log('[AuthContext] Contact sync skipped: no collectionId or disabled');
11325
+ return null;
11326
+ }
11327
+ try {
11328
+ console.log('[AuthContext] Syncing contact for user:', authUser.uid);
11329
+ const result = await smartlinks__namespace.contact.publicUpsert(collectionId, {
11330
+ userId: authUser.uid,
11331
+ email: authUser.email,
11332
+ displayName: authUser.displayName,
11333
+ phone: authUser.phoneNumber,
11334
+ customFields: customFields || {},
11335
+ source: 'authkit',
11336
+ });
11337
+ console.log('[AuthContext] Contact synced:', result.contactId);
11338
+ // Store contact ID locally
11339
+ if (!proxyMode) {
11340
+ await tokenStorage.saveContactId(result.contactId);
11341
+ }
11342
+ setContactId(result.contactId);
11343
+ // Fetch full contact to get customFields
11344
+ try {
11345
+ const fullContact = await smartlinks__namespace.contact.lookup(collectionId, {
11346
+ email: authUser.email
11347
+ });
11348
+ setContact(fullContact);
11349
+ notifyAuthStateChange('CONTACT_SYNCED', authUser, token, accountData, accountInfo, isVerified, fullContact, result.contactId);
11350
+ }
11351
+ catch (lookupErr) {
11352
+ console.warn('[AuthContext] Failed to lookup full contact:', lookupErr);
11353
+ }
11354
+ return result.contactId;
11355
+ }
11356
+ catch (err) {
11357
+ console.warn('[AuthContext] Contact sync failed (non-blocking):', err);
11358
+ return null;
11359
+ }
11360
+ }, [collectionId, shouldSyncContacts, proxyMode, token, accountData, accountInfo, isVerified, notifyAuthStateChange]);
11361
+ // Track interaction event (non-blocking)
11362
+ const trackInteraction = React.useCallback(async (eventType, userId, currentContactId, metadata) => {
11363
+ if (!collectionId || !shouldTrackInteractions) {
11364
+ console.log('[AuthContext] Interaction tracking skipped: no collectionId or disabled');
11365
+ return;
11366
+ }
11367
+ const interactionIdMap = {
11368
+ login: interactionConfig?.login || 'authkit.login',
11369
+ logout: interactionConfig?.logout || 'authkit.logout',
11370
+ signup: interactionConfig?.signup || 'authkit.signup',
11371
+ session_restore: interactionConfig?.sessionRestore,
11372
+ };
11373
+ const interactionId = interactionIdMap[eventType];
11374
+ if (!interactionId) {
11375
+ console.log(`[AuthContext] No interaction ID for ${eventType}, skipping`);
11376
+ return;
11377
+ }
11378
+ try {
11379
+ console.log(`[AuthContext] Tracking interaction: ${interactionId}`);
11380
+ await smartlinks__namespace.interactions.submitPublicEvent(collectionId, {
11381
+ collectionId,
11382
+ interactionId,
11383
+ userId,
11384
+ contactId: currentContactId || undefined,
11385
+ appId: interactionAppId,
11386
+ eventType,
11387
+ outcome: 'completed',
11388
+ metadata: {
11389
+ ...metadata,
11390
+ timestamp: new Date().toISOString(),
11391
+ source: 'authkit',
11392
+ },
11393
+ });
11394
+ console.log(`[AuthContext] Tracked interaction: ${interactionId}`);
11395
+ notifyAuthStateChange('INTERACTION_TRACKED', user, token, accountData, accountInfo, isVerified, contact, contactId);
11396
+ }
11397
+ catch (err) {
11398
+ console.warn('[AuthContext] Interaction tracking failed (non-blocking):', err);
11399
+ }
11400
+ }, [collectionId, shouldTrackInteractions, interactionAppId, interactionConfig, user, token, accountData, accountInfo, isVerified, contact, contactId, notifyAuthStateChange]);
11401
+ // Get contact (with optional refresh)
11402
+ const getContact = React.useCallback(async (forceRefresh = false) => {
11403
+ if (!collectionId) {
11404
+ console.log('[AuthContext] getContact: no collectionId');
11405
+ return null;
11406
+ }
11407
+ // Need either email or userId to lookup contact
11408
+ if (!user?.email && !user?.uid) {
11409
+ console.log('[AuthContext] getContact: no user email or uid');
11410
+ return null;
11411
+ }
11412
+ if (contact && !forceRefresh) {
11413
+ return contact;
11414
+ }
11415
+ try {
11416
+ // Prefer email lookup, fallback to userId for phone-only users
11417
+ const lookupParams = user.email
11418
+ ? { email: user.email }
11419
+ : { userId: user.uid };
11420
+ console.log('[AuthContext] Fetching contact with:', lookupParams);
11421
+ const result = await smartlinks__namespace.contact.lookup(collectionId, lookupParams);
11422
+ setContact(result);
11423
+ setContactId(result.contactId);
11424
+ return result;
11425
+ }
11426
+ catch (err) {
11427
+ console.warn('[AuthContext] Failed to get contact:', err);
11428
+ return null;
11429
+ }
11430
+ }, [collectionId, user, contact]);
11431
+ // Update contact custom fields
11432
+ const updateContactCustomFields = React.useCallback(async (customFields) => {
11433
+ if (!collectionId || !contactId) {
11434
+ throw new Error('No contact to update. Ensure collectionId is provided and user is synced.');
11435
+ }
11436
+ console.log('[AuthContext] Updating contact custom fields:', contactId);
11437
+ const updated = await smartlinks__namespace.contact.update(collectionId, contactId, { customFields });
11438
+ setContact(updated);
11439
+ return updated;
11440
+ }, [collectionId, contactId]);
11441
+ // Initialize auth state
11268
11442
  React.useEffect(() => {
11269
- // Prevent concurrent initialization only
11270
11443
  if (initializingRef.current) {
11271
11444
  console.log('[AuthContext] Skipping initialization - already in progress');
11272
11445
  return;
@@ -11280,12 +11453,9 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11280
11453
  console.log('[AuthContext] Proxy mode: checking for existing session via auth.getAccount()');
11281
11454
  try {
11282
11455
  const accountResponse = await smartlinks__namespace.auth.getAccount();
11283
- // auth.getAccount() always returns a response object, but uid will be
11284
- // empty/undefined if no user is logged in
11285
11456
  const accountAny = accountResponse;
11286
11457
  const hasValidSession = accountAny?.uid && accountAny.uid.length > 0;
11287
11458
  if (hasValidSession && isMounted) {
11288
- // User is logged in with valid account
11289
11459
  const userFromAccount = {
11290
11460
  uid: accountAny.uid,
11291
11461
  email: accountAny?.email,
@@ -11295,8 +11465,11 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11295
11465
  setUser(userFromAccount);
11296
11466
  setAccountData(accountResponse);
11297
11467
  setAccountInfo(accountResponse);
11468
+ setIsVerified(true);
11298
11469
  console.log('[AuthContext] Proxy mode: initialized from parent account, uid:', accountAny.uid);
11299
- notifyAuthStateChange('LOGIN', userFromAccount, null, accountResponse, accountResponse);
11470
+ notifyAuthStateChange('LOGIN', userFromAccount, null, accountResponse, accountResponse, true);
11471
+ // Sync contact in background (proxy mode)
11472
+ syncContact(userFromAccount, accountResponse);
11300
11473
  }
11301
11474
  else if (isMounted) {
11302
11475
  console.log('[AuthContext] Proxy mode: no valid session (no uid), awaiting login');
@@ -11311,29 +11484,56 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11311
11484
  }
11312
11485
  return;
11313
11486
  }
11314
- // STANDALONE MODE: Load from persistent storage
11487
+ // STANDALONE MODE: Optimistic restoration with background verification
11315
11488
  const storedToken = await tokenStorage.getToken();
11316
11489
  const storedUser = await tokenStorage.getUser();
11317
11490
  const storedAccountData = await tokenStorage.getAccountData();
11491
+ const storedContactId = await tokenStorage.getContactId();
11318
11492
  if (storedToken && storedUser) {
11319
- // Verify token FIRST before setting state
11493
+ if (isMounted) {
11494
+ setToken(storedToken.token);
11495
+ setUser(storedUser);
11496
+ setAccountData(storedAccountData);
11497
+ if (storedContactId) {
11498
+ setContactId(storedContactId);
11499
+ }
11500
+ console.log('[AuthContext] Session restored optimistically (pending verification)');
11501
+ notifyAuthStateChange('SESSION_RESTORED_OFFLINE', storedUser, storedToken.token, storedAccountData || null, null, false, null, storedContactId);
11502
+ }
11503
+ // BACKGROUND: Verify token
11320
11504
  try {
11321
- console.log('[AuthContext] Verifying stored token...');
11505
+ console.log('[AuthContext] Verifying stored token in background...');
11322
11506
  await smartlinks__namespace.auth.verifyToken(storedToken.token);
11323
- // Only set state if verification succeeded and component still mounted
11324
11507
  if (isMounted) {
11325
- setToken(storedToken.token);
11326
- setUser(storedUser);
11327
- setAccountData(storedAccountData);
11328
- console.log('[AuthContext] Session restored successfully');
11329
- // Notify subscribers that session was restored (critical for app to know we're logged in)
11330
- notifyAuthStateChange('LOGIN', storedUser, storedToken.token, storedAccountData || null);
11508
+ setIsVerified(true);
11509
+ pendingVerificationRef.current = false;
11510
+ console.log('[AuthContext] Session verified successfully');
11511
+ notifyAuthStateChange('SESSION_VERIFIED', storedUser, storedToken.token, storedAccountData || null, null, true, null, storedContactId);
11512
+ // Track session restore interaction (optional)
11513
+ if (interactionConfig?.sessionRestore) {
11514
+ trackInteraction('session_restore', storedUser.uid, storedContactId);
11515
+ }
11331
11516
  }
11332
11517
  }
11333
11518
  catch (err) {
11334
- console.warn('[AuthContext] Token verification failed, clearing stored credentials:', err);
11335
- await tokenStorage.clearAll();
11336
- // Don't set user state - leave as logged out
11519
+ if (isNetworkError(err)) {
11520
+ console.warn('[AuthContext] Network error during verification, will retry on reconnect:', err);
11521
+ pendingVerificationRef.current = true;
11522
+ }
11523
+ else {
11524
+ console.warn('[AuthContext] Token verification failed (auth error), clearing credentials:', err);
11525
+ if (isMounted) {
11526
+ setToken(null);
11527
+ setUser(null);
11528
+ setAccountData(null);
11529
+ setContactId(null);
11530
+ setContact(null);
11531
+ setIsVerified(false);
11532
+ pendingVerificationRef.current = false;
11533
+ }
11534
+ await tokenStorage.clearAll();
11535
+ notifyAuthStateChange('LOGOUT', null, null, null, null, false);
11536
+ }
11337
11537
  }
11338
11538
  }
11339
11539
  // Load cached account info if available
@@ -11355,18 +11555,16 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11355
11555
  }
11356
11556
  };
11357
11557
  initializeAuth();
11358
- // Cleanup for hot reload
11359
11558
  return () => {
11360
11559
  isMounted = false;
11361
11560
  };
11362
- }, [proxyMode, notifyAuthStateChange]);
11561
+ }, [proxyMode, notifyAuthStateChange, isNetworkError, syncContact, trackInteraction, interactionConfig?.sessionRestore]);
11363
11562
  // Listen for parent auth state changes (proxy mode only)
11364
11563
  React.useEffect(() => {
11365
11564
  if (!proxyMode)
11366
11565
  return;
11367
11566
  console.log('[AuthContext] Proxy mode: setting up parent message listener');
11368
11567
  const handleParentMessage = (event) => {
11369
- // Handle auth state pushed from parent
11370
11568
  if (event.data?.type === 'smartlinks:authkit:state') {
11371
11569
  const { user: parentUser, accountData: parentAccountData, authenticated } = event.data.payload || {};
11372
11570
  console.log('[AuthContext] Proxy mode: received state from parent:', { authenticated });
@@ -11380,15 +11578,20 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11380
11578
  setUser(userObj);
11381
11579
  setAccountData(parentAccountData || null);
11382
11580
  setAccountInfo(parentAccountData || null);
11383
- notifyAuthStateChange('CROSS_TAB_SYNC', userObj, null, parentAccountData || null, parentAccountData || null);
11581
+ setIsVerified(true);
11582
+ notifyAuthStateChange('CROSS_TAB_SYNC', userObj, null, parentAccountData || null, parentAccountData || null, true);
11583
+ // Sync contact on cross-tab state
11584
+ syncContact(userObj, parentAccountData);
11384
11585
  }
11385
11586
  else {
11386
- // Parent indicates no session / logged out
11387
11587
  setUser(null);
11388
11588
  setToken(null);
11389
11589
  setAccountData(null);
11390
11590
  setAccountInfo(null);
11391
- notifyAuthStateChange('LOGOUT', null, null, null, null);
11591
+ setContact(null);
11592
+ setContactId(null);
11593
+ setIsVerified(false);
11594
+ notifyAuthStateChange('LOGOUT', null, null, null, null, false);
11392
11595
  }
11393
11596
  }
11394
11597
  };
@@ -11397,54 +11600,60 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11397
11600
  console.log('[AuthContext] Proxy mode: cleaning up parent message listener');
11398
11601
  window.removeEventListener('message', handleParentMessage);
11399
11602
  };
11400
- }, [proxyMode, notifyAuthStateChange]);
11603
+ }, [proxyMode, notifyAuthStateChange, syncContact]);
11401
11604
  // Cross-tab synchronization - standalone mode only
11402
11605
  React.useEffect(() => {
11403
11606
  if (proxyMode)
11404
- return; // Skip cross-tab sync in proxy mode
11607
+ return;
11405
11608
  console.log('[AuthContext] Setting up cross-tab synchronization');
11406
11609
  const unsubscribe = onStorageChange(async (event) => {
11407
11610
  console.log('[AuthContext] Cross-tab storage event:', event.type, event.key);
11408
11611
  try {
11409
11612
  if (event.type === 'clear') {
11410
- // Another tab cleared all storage (logout)
11411
11613
  console.log('[AuthContext] Detected logout in another tab');
11412
11614
  setToken(null);
11413
11615
  setUser(null);
11414
11616
  setAccountData(null);
11415
11617
  setAccountInfo(null);
11618
+ setContact(null);
11619
+ setContactId(null);
11620
+ setIsVerified(false);
11416
11621
  smartlinks__namespace.auth.logout();
11417
- notifyAuthStateChange('CROSS_TAB_SYNC', null, null, null);
11622
+ notifyAuthStateChange('CROSS_TAB_SYNC', null, null, null, null, false);
11418
11623
  }
11419
11624
  else if (event.type === 'remove' && (event.key === 'token' || event.key === 'user')) {
11420
- // Another tab removed token or user (logout)
11421
11625
  console.log('[AuthContext] Detected token/user removal in another tab');
11422
11626
  setToken(null);
11423
11627
  setUser(null);
11424
11628
  setAccountData(null);
11425
11629
  setAccountInfo(null);
11630
+ setContact(null);
11631
+ setContactId(null);
11632
+ setIsVerified(false);
11426
11633
  smartlinks__namespace.auth.logout();
11427
- notifyAuthStateChange('CROSS_TAB_SYNC', null, null, null);
11634
+ notifyAuthStateChange('CROSS_TAB_SYNC', null, null, null, null, false);
11428
11635
  }
11429
11636
  else if (event.type === 'set' && event.key === 'token') {
11430
- // Another tab set a new token (login)
11431
11637
  console.log('[AuthContext] Detected login in another tab');
11432
11638
  const storedToken = await tokenStorage.getToken();
11433
11639
  const storedUser = await tokenStorage.getUser();
11434
11640
  const storedAccountData = await tokenStorage.getAccountData();
11641
+ const storedContactId = await tokenStorage.getContactId();
11435
11642
  if (storedToken && storedUser) {
11436
11643
  setToken(storedToken.token);
11437
11644
  setUser(storedUser);
11438
11645
  setAccountData(storedAccountData);
11439
- // Set bearer token in global Smartlinks SDK
11646
+ if (storedContactId) {
11647
+ setContactId(storedContactId);
11648
+ }
11649
+ setIsVerified(true);
11440
11650
  smartlinks__namespace.auth.verifyToken(storedToken.token).catch(err => {
11441
11651
  console.warn('[AuthContext] Failed to restore bearer token from cross-tab sync:', err);
11442
11652
  });
11443
- notifyAuthStateChange('CROSS_TAB_SYNC', storedUser, storedToken.token, storedAccountData);
11653
+ notifyAuthStateChange('CROSS_TAB_SYNC', storedUser, storedToken.token, storedAccountData, null, true, null, storedContactId);
11444
11654
  }
11445
11655
  }
11446
11656
  else if (event.type === 'set' && event.key === 'account_info') {
11447
- // Another tab fetched fresh account info
11448
11657
  const cached = await tokenStorage.getAccountInfo();
11449
11658
  if (cached && !cached.isStale) {
11450
11659
  setAccountInfo(cached.data);
@@ -11461,7 +11670,7 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11461
11670
  unsubscribe();
11462
11671
  };
11463
11672
  }, [proxyMode, notifyAuthStateChange]);
11464
- const login = React.useCallback(async (authToken, authUser, authAccountData) => {
11673
+ const login = React.useCallback(async (authToken, authUser, authAccountData, isNewUser) => {
11465
11674
  try {
11466
11675
  // Only persist to storage in standalone mode
11467
11676
  if (!proxyMode) {
@@ -11470,7 +11679,6 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11470
11679
  if (authAccountData) {
11471
11680
  await tokenStorage.saveAccountData(authAccountData);
11472
11681
  }
11473
- // Set bearer token in global Smartlinks SDK via auth.verifyToken
11474
11682
  smartlinks__namespace.auth.verifyToken(authToken).catch(err => {
11475
11683
  console.warn('Failed to set bearer token on login:', err);
11476
11684
  });
@@ -11479,8 +11687,9 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11479
11687
  setToken(authToken);
11480
11688
  setUser(authUser);
11481
11689
  setAccountData(authAccountData || null);
11690
+ setIsVerified(true);
11691
+ pendingVerificationRef.current = false;
11482
11692
  // Cross-iframe auth state synchronization
11483
- // Always notify parent frame of login (both modes, but especially important in proxy mode)
11484
11693
  if (smartlinks.iframe.isIframe()) {
11485
11694
  console.log('[AuthContext] Notifying parent of login via postMessage');
11486
11695
  smartlinks.iframe.sendParentCustom('smartlinks:authkit:login', {
@@ -11489,7 +11698,13 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11489
11698
  accountData: authAccountData || null
11490
11699
  });
11491
11700
  }
11492
- notifyAuthStateChange('LOGIN', authUser, authToken, authAccountData || null);
11701
+ notifyAuthStateChange('LOGIN', authUser, authToken, authAccountData || null, null, true);
11702
+ // Sync contact (non-blocking)
11703
+ const newContactId = await syncContact(authUser, authAccountData);
11704
+ // Track interaction (non-blocking)
11705
+ trackInteraction(isNewUser ? 'signup' : 'login', authUser.uid, newContactId, {
11706
+ provider: authUser.email ? 'email' : 'phone',
11707
+ });
11493
11708
  // Optionally preload account info on login (standalone mode only)
11494
11709
  if (!proxyMode && preloadAccountInfo) {
11495
11710
  getAccount(true).catch(error => {
@@ -11501,8 +11716,10 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11501
11716
  console.error('Failed to save auth data to storage:', error);
11502
11717
  throw error;
11503
11718
  }
11504
- }, [proxyMode, notifyAuthStateChange, preloadAccountInfo]);
11719
+ }, [proxyMode, notifyAuthStateChange, preloadAccountInfo, syncContact, trackInteraction]);
11505
11720
  const logout = React.useCallback(async () => {
11721
+ const currentUser = user;
11722
+ const currentContactId = contactId;
11506
11723
  try {
11507
11724
  // Only clear persistent storage in standalone mode
11508
11725
  if (!proxyMode) {
@@ -11514,21 +11731,27 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11514
11731
  setUser(null);
11515
11732
  setAccountData(null);
11516
11733
  setAccountInfo(null);
11734
+ setContact(null);
11735
+ setContactId(null);
11736
+ setIsVerified(false);
11737
+ pendingVerificationRef.current = false;
11517
11738
  // Cross-iframe auth state synchronization
11518
- // Always notify parent frame of logout
11519
11739
  if (smartlinks.iframe.isIframe()) {
11520
11740
  console.log('[AuthContext] Notifying parent of logout via postMessage');
11521
11741
  smartlinks.iframe.sendParentCustom('smartlinks:authkit:logout', {});
11522
11742
  }
11523
- notifyAuthStateChange('LOGOUT', null, null, null);
11743
+ notifyAuthStateChange('LOGOUT', null, null, null, null, false);
11744
+ // Track logout interaction (fire and forget)
11745
+ if (currentUser && collectionId && shouldTrackInteractions) {
11746
+ trackInteraction('logout', currentUser.uid, currentContactId);
11747
+ }
11524
11748
  }
11525
11749
  catch (error) {
11526
11750
  console.error('Failed to clear auth data from storage:', error);
11527
11751
  }
11528
- }, [proxyMode, notifyAuthStateChange]);
11752
+ }, [proxyMode, notifyAuthStateChange, user, contactId, collectionId, shouldTrackInteractions, trackInteraction]);
11529
11753
  const getToken = React.useCallback(async () => {
11530
11754
  if (proxyMode) {
11531
- // In proxy mode, token is managed by parent - return memory state
11532
11755
  return token;
11533
11756
  }
11534
11757
  const storedToken = await tokenStorage.getToken();
@@ -11537,23 +11760,19 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11537
11760
  const refreshToken = React.useCallback(async () => {
11538
11761
  throw new Error('Token refresh must be implemented via your backend API');
11539
11762
  }, []);
11540
- // Get account with intelligent caching (or direct parent fetch in proxy mode)
11541
11763
  const getAccount = React.useCallback(async (forceRefresh = false) => {
11542
11764
  try {
11543
11765
  if (proxyMode) {
11544
- // PROXY MODE: Always fetch from parent via proxied API, no local cache
11545
11766
  console.log('[AuthContext] Proxy mode: fetching account from parent');
11546
11767
  const freshAccountInfo = await smartlinks__namespace.auth.getAccount();
11547
11768
  setAccountInfo(freshAccountInfo);
11548
11769
  setAccountData(freshAccountInfo);
11549
- notifyAuthStateChange('ACCOUNT_REFRESH', user, token, freshAccountInfo, freshAccountInfo);
11770
+ notifyAuthStateChange('ACCOUNT_REFRESH', user, token, freshAccountInfo, freshAccountInfo, isVerified, contact, contactId);
11550
11771
  return freshAccountInfo;
11551
11772
  }
11552
- // STANDALONE MODE: Use caching
11553
11773
  if (!token) {
11554
11774
  throw new Error('Not authenticated. Please login first.');
11555
11775
  }
11556
- // Check cache unless force refresh
11557
11776
  if (!forceRefresh) {
11558
11777
  const cached = await tokenStorage.getAccountInfo();
11559
11778
  if (cached && !cached.isStale) {
@@ -11561,18 +11780,15 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11561
11780
  return cached.data;
11562
11781
  }
11563
11782
  }
11564
- // Fetch fresh data from API
11565
11783
  console.log('[AuthContext] Fetching fresh account info from API');
11566
11784
  const freshAccountInfo = await smartlinks__namespace.auth.getAccount();
11567
- // Cache the fresh data
11568
11785
  await tokenStorage.saveAccountInfo(freshAccountInfo, accountCacheTTL);
11569
11786
  setAccountInfo(freshAccountInfo);
11570
- notifyAuthStateChange('ACCOUNT_REFRESH', user, token, accountData, freshAccountInfo);
11787
+ notifyAuthStateChange('ACCOUNT_REFRESH', user, token, accountData, freshAccountInfo, isVerified, contact, contactId);
11571
11788
  return freshAccountInfo;
11572
11789
  }
11573
11790
  catch (error) {
11574
11791
  console.error('[AuthContext] Failed to get account info:', error);
11575
- // Fallback to stale cache if API fails (standalone mode only)
11576
11792
  if (!proxyMode) {
11577
11793
  const cached = await tokenStorage.getAccountInfo();
11578
11794
  if (cached) {
@@ -11582,12 +11798,10 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11582
11798
  }
11583
11799
  throw error;
11584
11800
  }
11585
- }, [proxyMode, token, accountCacheTTL, user, accountData, notifyAuthStateChange]);
11586
- // Convenience method for explicit refresh
11801
+ }, [proxyMode, token, accountCacheTTL, user, accountData, isVerified, contact, contactId, notifyAuthStateChange]);
11587
11802
  const refreshAccount = React.useCallback(async () => {
11588
11803
  return await getAccount(true);
11589
11804
  }, [getAccount]);
11590
- // Clear account cache (no-op in proxy mode)
11591
11805
  const clearAccountCache = React.useCallback(async () => {
11592
11806
  if (!proxyMode) {
11593
11807
  await tokenStorage.clearAccountInfo();
@@ -11596,19 +11810,77 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11596
11810
  }, [proxyMode]);
11597
11811
  const onAuthStateChange = React.useCallback((callback) => {
11598
11812
  callbacksRef.current.add(callback);
11599
- // Return unsubscribe function
11600
11813
  return () => {
11601
11814
  callbacksRef.current.delete(callback);
11602
11815
  };
11603
11816
  }, []);
11817
+ const retryVerification = React.useCallback(async () => {
11818
+ if (!token || !user) {
11819
+ console.log('[AuthContext] No session to verify');
11820
+ return false;
11821
+ }
11822
+ if (isVerified) {
11823
+ console.log('[AuthContext] Session already verified');
11824
+ return true;
11825
+ }
11826
+ try {
11827
+ console.log('[AuthContext] Retrying session verification...');
11828
+ await smartlinks__namespace.auth.verifyToken(token);
11829
+ setIsVerified(true);
11830
+ pendingVerificationRef.current = false;
11831
+ console.log('[AuthContext] Session verified on retry');
11832
+ notifyAuthStateChange('SESSION_VERIFIED', user, token, accountData, accountInfo, true, contact, contactId);
11833
+ return true;
11834
+ }
11835
+ catch (err) {
11836
+ if (isNetworkError(err)) {
11837
+ console.warn('[AuthContext] Network still unavailable, will retry later');
11838
+ return false;
11839
+ }
11840
+ else {
11841
+ console.warn('[AuthContext] Session invalid on retry, logging out');
11842
+ await logout();
11843
+ return false;
11844
+ }
11845
+ }
11846
+ }, [token, user, isVerified, accountData, accountInfo, contact, contactId, notifyAuthStateChange, isNetworkError, logout]);
11847
+ // Online/offline event listener for auto-retry verification
11848
+ React.useEffect(() => {
11849
+ if (proxyMode)
11850
+ return;
11851
+ const handleOnline = () => {
11852
+ console.log('[AuthContext] Network reconnected');
11853
+ setIsOnline(true);
11854
+ if (pendingVerificationRef.current && token && user) {
11855
+ console.log('[AuthContext] Retrying pending verification after reconnect...');
11856
+ retryVerification();
11857
+ }
11858
+ };
11859
+ const handleOffline = () => {
11860
+ console.log('[AuthContext] Network disconnected');
11861
+ setIsOnline(false);
11862
+ };
11863
+ window.addEventListener('online', handleOnline);
11864
+ window.addEventListener('offline', handleOffline);
11865
+ return () => {
11866
+ window.removeEventListener('online', handleOnline);
11867
+ window.removeEventListener('offline', handleOffline);
11868
+ };
11869
+ }, [proxyMode, token, user, retryVerification]);
11604
11870
  const value = {
11605
11871
  user,
11606
11872
  token,
11607
11873
  accountData,
11608
11874
  accountInfo,
11609
11875
  isAuthenticated: !!user,
11876
+ isVerified,
11610
11877
  isLoading,
11878
+ isOnline,
11611
11879
  proxyMode,
11880
+ contact,
11881
+ contactId,
11882
+ getContact,
11883
+ updateContactCustomFields,
11612
11884
  login,
11613
11885
  logout,
11614
11886
  getToken,
@@ -11617,6 +11889,7 @@ const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 *
11617
11889
  refreshAccount,
11618
11890
  clearAccountCache,
11619
11891
  onAuthStateChange,
11892
+ retryVerification,
11620
11893
  };
11621
11894
  return jsxRuntime.jsx(AuthContext.Provider, { value: value, children: children });
11622
11895
  };
@@ -11895,7 +12168,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11895
12168
  const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
11896
12169
  if ((verificationMode === 'verify-then-auto-login' || verificationMode === 'immediate') && response.token) {
11897
12170
  // Auto-login modes: Log the user in immediately if token is provided
11898
- auth.login(response.token, response.user, response.accountData);
12171
+ auth.login(response.token, response.user, response.accountData, true);
11899
12172
  setAuthSuccess(true);
11900
12173
  setSuccessMessage('Email verified successfully! You are now logged in.');
11901
12174
  onAuthSuccess(response.token, response.user, response.accountData);
@@ -11938,7 +12211,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11938
12211
  const response = await api.verifyMagicLink(token);
11939
12212
  // Auto-login with magic link if token is provided
11940
12213
  if (response.token) {
11941
- auth.login(response.token, response.user, response.accountData);
12214
+ auth.login(response.token, response.user, response.accountData, true);
11942
12215
  setAuthSuccess(true);
11943
12216
  setSuccessMessage('Magic link verified! You are now logged in.');
11944
12217
  onAuthSuccess(response.token, response.user, response.accountData);
@@ -12013,8 +12286,8 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12013
12286
  if (mode === 'register') {
12014
12287
  // Handle different verification modes
12015
12288
  if (verificationMode === 'immediate' && response.token) {
12016
- // Immediate mode: Log in right away if token is provided
12017
- auth.login(response.token, response.user, response.accountData);
12289
+ // Immediate mode: Log in right away if token is provided (isNewUser=true for registration)
12290
+ auth.login(response.token, response.user, response.accountData, true);
12018
12291
  setAuthSuccess(true);
12019
12292
  const deadline = response.emailVerificationDeadline
12020
12293
  ? new Date(response.emailVerificationDeadline).toLocaleString()
@@ -12050,7 +12323,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12050
12323
  if (response.requiresEmailVerification) {
12051
12324
  throw new Error('Please verify your email before logging in. Check your inbox for the verification link.');
12052
12325
  }
12053
- auth.login(response.token, response.user, response.accountData);
12326
+ auth.login(response.token, response.user, response.accountData, false);
12054
12327
  setAuthSuccess(true);
12055
12328
  setSuccessMessage('Login successful!');
12056
12329
  onAuthSuccess(response.token, response.user, response.accountData);
@@ -12210,7 +12483,8 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12210
12483
  tokenType: 'access_token',
12211
12484
  });
12212
12485
  if (authResponse.token) {
12213
- auth.login(authResponse.token, authResponse.user, authResponse.accountData);
12486
+ // Google OAuth can be login or signup - use isNewUser flag from backend if available
12487
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser);
12214
12488
  setAuthSuccess(true);
12215
12489
  setSuccessMessage('Google login successful!');
12216
12490
  onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
@@ -12259,7 +12533,8 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12259
12533
  const idToken = response.credential;
12260
12534
  const authResponse = await api.loginWithGoogle(idToken);
12261
12535
  if (authResponse.token) {
12262
- auth.login(authResponse.token, authResponse.user, authResponse.accountData);
12536
+ // Google OAuth can be login or signup - use isNewUser flag from backend if available
12537
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser);
12263
12538
  setAuthSuccess(true);
12264
12539
  setSuccessMessage('Google login successful!');
12265
12540
  onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
@@ -12320,7 +12595,8 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12320
12595
  const response = await api.verifyPhoneCode(phoneNumber, verificationCode);
12321
12596
  // Update auth context with account data if token is provided
12322
12597
  if (response.token) {
12323
- auth.login(response.token, response.user, response.accountData);
12598
+ // Phone auth can be login or signup - use isNewUser flag from backend if available
12599
+ auth.login(response.token, response.user, response.accountData, response.isNewUser);
12324
12600
  onAuthSuccess(response.token, response.user, response.accountData);
12325
12601
  if (redirectUrl) {
12326
12602
  window.location.href = redirectUrl;