@proveanything/smartlinks-auth-ui 0.5.12 → 0.5.14

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
@@ -10893,18 +10893,19 @@ const isDesktop = () => {
10893
10893
  return false;
10894
10894
  return !/Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent);
10895
10895
  };
10896
- const WhatsAppAuthForm = ({ onSend, onPollStatus, onVerified, onBack, loading = false, error, collectName = false, pollIntervalMs = 3000, }) => {
10896
+ const WhatsAppAuthForm = ({ onSend, onPollStatus, onVerified, onBack, loading = false, error, collectName = false, pollIntervalMs = 3000, initialSent = null, }) => {
10897
10897
  const [displayName, setDisplayName] = React.useState('');
10898
- const [sent, setSent] = React.useState(null);
10898
+ const [sent, setSent] = React.useState(initialSent ?? null);
10899
10899
  const [pollError, setPollError] = React.useState();
10900
10900
  const [qrDataUrl, setQrDataUrl] = React.useState(null);
10901
10901
  const [showOnDesktop] = React.useState(isDesktop());
10902
10902
  const verifiedFiredRef = React.useRef(false);
10903
10903
  const autoSentRef = React.useRef(false);
10904
+ const hydratedInitialTokenRef = React.useRef(initialSent?.token ?? null);
10904
10905
  // Auto-issue the WhatsApp link on mount when we don't need to collect a name.
10905
10906
  // When collectName is true, wait for the user to submit the name first.
10906
10907
  React.useEffect(() => {
10907
- if (collectName)
10908
+ if (collectName || sent)
10908
10909
  return;
10909
10910
  if (autoSentRef.current)
10910
10911
  return;
@@ -10912,13 +10913,24 @@ const WhatsAppAuthForm = ({ onSend, onPollStatus, onVerified, onBack, loading =
10912
10913
  (async () => {
10913
10914
  try {
10914
10915
  const result = await onSend();
10916
+ hydratedInitialTokenRef.current = result.token;
10915
10917
  setSent(result);
10916
10918
  }
10917
10919
  catch {
10918
10920
  // Surfaced via parent error prop
10919
10921
  }
10920
10922
  })();
10921
- }, [collectName, onSend]);
10923
+ }, [collectName, onSend, sent]);
10924
+ React.useEffect(() => {
10925
+ if (!initialSent?.token)
10926
+ return;
10927
+ if (initialSent.token === hydratedInitialTokenRef.current)
10928
+ return;
10929
+ hydratedInitialTokenRef.current = initialSent.token;
10930
+ verifiedFiredRef.current = false;
10931
+ setPollError(undefined);
10932
+ setSent(initialSent);
10933
+ }, [initialSent]);
10922
10934
  // Generate QR code for the WhatsApp deep-link (great for desktop scan-with-phone UX)
10923
10935
  React.useEffect(() => {
10924
10936
  if (!sent?.waLink)
@@ -10947,15 +10959,26 @@ const WhatsAppAuthForm = ({ onSend, onPollStatus, onVerified, onBack, loading =
10947
10959
  return;
10948
10960
  let stopped = false;
10949
10961
  let timer;
10962
+ let pollInFlight = false;
10963
+ const scheduleNext = () => {
10964
+ if (stopped)
10965
+ return;
10966
+ timer = setTimeout(tick, pollIntervalMs);
10967
+ };
10950
10968
  const tick = async () => {
10969
+ if (pollInFlight)
10970
+ return;
10971
+ pollInFlight = true;
10951
10972
  try {
10952
10973
  const status = await onPollStatus(sent.token);
10953
10974
  if (stopped)
10954
10975
  return;
10955
10976
  if (status.verified && !verifiedFiredRef.current) {
10956
- verifiedFiredRef.current = true;
10957
- onVerified(status);
10958
- return;
10977
+ const handled = await onVerified(status);
10978
+ if (handled !== false) {
10979
+ verifiedFiredRef.current = true;
10980
+ return;
10981
+ }
10959
10982
  }
10960
10983
  if (status.status === 'failed' || status.status === 'expired') {
10961
10984
  setPollError(status.status === 'expired'
@@ -10967,12 +10990,31 @@ const WhatsAppAuthForm = ({ onSend, onPollStatus, onVerified, onBack, loading =
10967
10990
  catch {
10968
10991
  // Transient errors — keep polling
10969
10992
  }
10970
- timer = setTimeout(tick, pollIntervalMs);
10993
+ finally {
10994
+ pollInFlight = false;
10995
+ }
10996
+ scheduleNext();
10997
+ };
10998
+ const handleResume = () => {
10999
+ if (document.visibilityState && document.visibilityState !== 'visible')
11000
+ return;
11001
+ tick().catch?.(() => { });
11002
+ };
11003
+ const handleVisibilityChange = () => {
11004
+ if (document.visibilityState === 'visible') {
11005
+ handleResume();
11006
+ }
10971
11007
  };
10972
- timer = setTimeout(tick, pollIntervalMs);
11008
+ tick().catch?.(() => { });
11009
+ window.addEventListener('focus', handleResume);
11010
+ window.addEventListener('pageshow', handleResume);
11011
+ document.addEventListener('visibilitychange', handleVisibilityChange);
10973
11012
  return () => {
10974
11013
  stopped = true;
10975
11014
  clearTimeout(timer);
11015
+ window.removeEventListener('focus', handleResume);
11016
+ window.removeEventListener('pageshow', handleResume);
11017
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
10976
11018
  };
10977
11019
  }, [sent?.token, onPollStatus, onVerified, pollIntervalMs]);
10978
11020
  const handleNameSubmit = async (e) => {
@@ -10981,6 +11023,7 @@ const WhatsAppAuthForm = ({ onSend, onPollStatus, onVerified, onBack, loading =
10981
11023
  verifiedFiredRef.current = false;
10982
11024
  try {
10983
11025
  const result = await onSend(displayName.trim() || undefined);
11026
+ hydratedInitialTokenRef.current = result.token;
10984
11027
  setSent(result);
10985
11028
  }
10986
11029
  catch {
@@ -10995,6 +11038,7 @@ const WhatsAppAuthForm = ({ onSend, onPollStatus, onVerified, onBack, loading =
10995
11038
  if (!collectName) {
10996
11039
  try {
10997
11040
  const result = await onSend();
11041
+ hydratedInitialTokenRef.current = result.token;
10998
11042
  setSent(result);
10999
11043
  }
11000
11044
  catch {
@@ -12557,6 +12601,140 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
12557
12601
  unsubscribe();
12558
12602
  };
12559
12603
  }, [proxyMode, notifyAuthStateChange]);
12604
+ // Mobile app-switching can miss BroadcastChannel/storage events, especially on iOS.
12605
+ // Re-read persisted auth state whenever the page becomes active again so a
12606
+ // WhatsApp-returned tab/window can pick up a session that another page wrote.
12607
+ React.useEffect(() => {
12608
+ if (proxyMode)
12609
+ return;
12610
+ const rehydrateFromStorage = async () => {
12611
+ try {
12612
+ const storedToken = await tokenStorage.getToken();
12613
+ const storedUser = await tokenStorage.getUser();
12614
+ const storedAccountData = await tokenStorage.getAccountData();
12615
+ const storedAccountInfo = await tokenStorage.getAccountInfo();
12616
+ const storedContactId = await tokenStorage.getContactId();
12617
+ if (!storedToken || !storedUser)
12618
+ return;
12619
+ const tokenChanged = storedToken.token !== token;
12620
+ const userChanged = storedUser.uid !== user?.uid;
12621
+ if (tokenChanged || userChanged || !user) {
12622
+ setToken(storedToken.token);
12623
+ setUser(storedUser);
12624
+ setAccountData(storedAccountData);
12625
+ setAccountInfo(storedAccountInfo?.data || storedAccountData || null);
12626
+ setContactId(storedContactId);
12627
+ setIsVerified(true);
12628
+ notifyAuthStateChange('SESSION_RESTORED', storedUser, storedToken.token, storedAccountData || null, storedAccountInfo?.data || storedAccountData || null, true, contact, storedContactId);
12629
+ }
12630
+ if (!isVerified || pendingVerificationRef.current) {
12631
+ smartlinks__namespace.auth.verifyToken(storedToken.token)
12632
+ .then(() => {
12633
+ setIsVerified(true);
12634
+ pendingVerificationRef.current = false;
12635
+ })
12636
+ .catch((error) => {
12637
+ if (!isNetworkError(error)) {
12638
+ console.warn('[AuthContext] Resume verification failed:', error);
12639
+ }
12640
+ });
12641
+ }
12642
+ }
12643
+ catch (error) {
12644
+ console.warn('[AuthContext] Failed to rehydrate auth on resume:', error);
12645
+ }
12646
+ };
12647
+ const handleResume = () => {
12648
+ if (typeof document !== 'undefined' && document.visibilityState !== 'visible')
12649
+ return;
12650
+ rehydrateFromStorage().catch(() => { });
12651
+ };
12652
+ const handleVisibilityChange = () => {
12653
+ if (document.visibilityState === 'visible') {
12654
+ handleResume();
12655
+ }
12656
+ };
12657
+ window.addEventListener('focus', handleResume);
12658
+ window.addEventListener('pageshow', handleResume);
12659
+ document.addEventListener('visibilitychange', handleVisibilityChange);
12660
+ return () => {
12661
+ window.removeEventListener('focus', handleResume);
12662
+ window.removeEventListener('pageshow', handleResume);
12663
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
12664
+ };
12665
+ }, [proxyMode, token, user, isVerified, notifyAuthStateChange, contact, isNetworkError]);
12666
+ // Mobile app-switching can also miss BOTH visibility/storage signals on the newly-opened
12667
+ // page (common with WhatsApp handoff on iOS/Android webviews). Keep a short-lived polling
12668
+ // window after mount while logged out so a session written by the background/original page
12669
+ // is still adopted without requiring a manual refresh.
12670
+ React.useEffect(() => {
12671
+ if (proxyMode || user || token)
12672
+ return;
12673
+ let cancelled = false;
12674
+ let intervalId = null;
12675
+ let timeoutId = null;
12676
+ let pollInFlight = false;
12677
+ const adoptStoredSession = async () => {
12678
+ if (cancelled || pollInFlight)
12679
+ return;
12680
+ pollInFlight = true;
12681
+ try {
12682
+ const storedToken = await tokenStorage.getToken();
12683
+ const storedUser = await tokenStorage.getUser();
12684
+ if (!storedToken?.token || !storedUser)
12685
+ return;
12686
+ const storedAccountData = await tokenStorage.getAccountData();
12687
+ const storedAccountInfo = await tokenStorage.getAccountInfo();
12688
+ const storedContactId = await tokenStorage.getContactId();
12689
+ if (cancelled)
12690
+ return;
12691
+ setToken(storedToken.token);
12692
+ setUser(storedUser);
12693
+ setAccountData(storedAccountData || null);
12694
+ setAccountInfo(storedAccountInfo?.data || storedAccountData || null);
12695
+ setContactId(storedContactId);
12696
+ setIsVerified(true);
12697
+ pendingVerificationRef.current = false;
12698
+ notifyAuthStateChange('SESSION_RESTORED', storedUser, storedToken.token, storedAccountData || null, storedAccountInfo?.data || storedAccountData || null, true, contact, storedContactId);
12699
+ smartlinks__namespace.auth.verifyToken(storedToken.token).catch((error) => {
12700
+ if (!isNetworkError(error)) {
12701
+ console.warn('[AuthContext] Background rehydrate verification failed:', error);
12702
+ }
12703
+ });
12704
+ if (intervalId) {
12705
+ clearInterval(intervalId);
12706
+ intervalId = null;
12707
+ }
12708
+ if (timeoutId) {
12709
+ clearTimeout(timeoutId);
12710
+ timeoutId = null;
12711
+ }
12712
+ }
12713
+ catch (error) {
12714
+ console.warn('[AuthContext] Failed to adopt stored session during mobile handoff:', error);
12715
+ }
12716
+ finally {
12717
+ pollInFlight = false;
12718
+ }
12719
+ };
12720
+ adoptStoredSession().catch(() => { });
12721
+ intervalId = setInterval(() => {
12722
+ adoptStoredSession().catch(() => { });
12723
+ }, 1000);
12724
+ timeoutId = setTimeout(() => {
12725
+ if (intervalId) {
12726
+ clearInterval(intervalId);
12727
+ intervalId = null;
12728
+ }
12729
+ }, 20000);
12730
+ return () => {
12731
+ cancelled = true;
12732
+ if (intervalId)
12733
+ clearInterval(intervalId);
12734
+ if (timeoutId)
12735
+ clearTimeout(timeoutId);
12736
+ };
12737
+ }, [proxyMode, user, token, notifyAuthStateChange, contact, isNetworkError]);
12560
12738
  // Helper: Send login to parent and wait for acknowledgment
12561
12739
  // Used for deep-link flows (email verification, magic link) where we need to ensure
12562
12740
  // the parent has persisted the session before redirecting (which causes page reload)
@@ -12590,10 +12768,15 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
12590
12768
  try {
12591
12769
  // Only persist to storage in standalone mode
12592
12770
  if (!proxyMode) {
12771
+ await tokenStorage.clearAccountInfo();
12593
12772
  await tokenStorage.saveToken(authToken, expiresAt);
12594
12773
  await tokenStorage.saveUser(authUser);
12595
12774
  if (authAccountData) {
12596
12775
  await tokenStorage.saveAccountData(authAccountData);
12776
+ await tokenStorage.saveAccountInfo(authAccountData, accountCacheTTL);
12777
+ }
12778
+ else {
12779
+ await tokenStorage.clearAccountData();
12597
12780
  }
12598
12781
  smartlinks__namespace.auth.verifyToken(authToken).catch(() => { });
12599
12782
  }
@@ -12601,6 +12784,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
12601
12784
  setToken(authToken);
12602
12785
  setUser(authUser);
12603
12786
  setAccountData(authAccountData || null);
12787
+ setAccountInfo(authAccountData || null);
12604
12788
  pendingVerificationRef.current = false;
12605
12789
  // Cross-iframe auth state synchronization
12606
12790
  // ALWAYS wait for parent acknowledgment in iframe mode before proceeding
@@ -12611,7 +12795,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
12611
12795
  }
12612
12796
  // NOW set isVerified - after parent has acknowledged and session is ready
12613
12797
  setIsVerified(true);
12614
- notifyAuthStateChange('LOGIN', authUser, authToken, authAccountData || null, null, true);
12798
+ notifyAuthStateChange('LOGIN', authUser, authToken, authAccountData || null, authAccountData || null, true);
12615
12799
  // Sync contact (non-blocking)
12616
12800
  const newContactId = await syncContact(authUser, authAccountData);
12617
12801
  // Track interaction (non-blocking)
@@ -12627,7 +12811,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
12627
12811
  console.error('Failed to save auth data to storage:', error);
12628
12812
  throw error;
12629
12813
  }
12630
- }, [proxyMode, notifyAuthStateChange, preloadAccountInfo, syncContact, trackInteraction]);
12814
+ }, [proxyMode, notifyAuthStateChange, preloadAccountInfo, syncContact, trackInteraction, accountCacheTTL]);
12631
12815
  const logout = React.useCallback(async () => {
12632
12816
  const currentUser = user;
12633
12817
  const currentContactId = contactId;
@@ -12938,6 +13122,22 @@ const normalizeQueryString = (query) => {
12938
13122
  const buildSearchParams = (rawQuery) => {
12939
13123
  return new URLSearchParams(normalizeQueryString(rawQuery));
12940
13124
  };
13125
+ const appendWhatsAppResumeParams = (url, token) => {
13126
+ try {
13127
+ const nextUrl = new URL(url, window.location.origin);
13128
+ nextUrl.searchParams.set('mode', 'whatsapp');
13129
+ if (token) {
13130
+ nextUrl.searchParams.set('token', token);
13131
+ }
13132
+ else {
13133
+ nextUrl.searchParams.delete('token');
13134
+ }
13135
+ return nextUrl.toString();
13136
+ }
13137
+ catch {
13138
+ return url;
13139
+ }
13140
+ };
12941
13141
  // Helper to check for URL auth params synchronously (runs during initialization)
12942
13142
  // This prevents the form from flashing before detecting deep-link flows
12943
13143
  const getInitialUrlAuthParams = () => {
@@ -12960,6 +13160,19 @@ const getExpirationFromResponse = (response) => {
12960
13160
  return Date.now() + response.expiresIn;
12961
13161
  return undefined; // Will use 7-day default in tokenStorage
12962
13162
  };
13163
+ const stripWhatsAppResumeParams = (url) => {
13164
+ try {
13165
+ const urlObj = new URL(url);
13166
+ if (urlObj.searchParams.get('mode') === 'whatsapp') {
13167
+ urlObj.searchParams.delete('mode');
13168
+ urlObj.searchParams.delete('token');
13169
+ }
13170
+ return urlObj.toString();
13171
+ }
13172
+ catch {
13173
+ return url;
13174
+ }
13175
+ };
12963
13176
  const getActionResultErrorMessage = (result) => {
12964
13177
  if (!result || typeof result !== 'object')
12965
13178
  return null;
@@ -13223,6 +13436,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13223
13436
  const [silentSignInChecked, setSilentSignInChecked] = React.useState(false); // Track if silent sign-in has been checked
13224
13437
  const [googleFallbackToPopup, setGoogleFallbackToPopup] = React.useState(false); // Show popup fallback when FedCM is blocked/dismissed
13225
13438
  const [googleNativeTimedOut, setGoogleNativeTimedOut] = React.useState(false); // Native bridge callback timed out
13439
+ const [restoredWhatsAppSend, setRestoredWhatsAppSend] = React.useState(null);
13226
13440
  const log = React.useMemo(() => createLoggerWrapper(logger), [logger]);
13227
13441
  const api = new AuthAPI(apiEndpoint, clientId, clientName, logger);
13228
13442
  const auth = useAuth();
@@ -13286,6 +13500,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13286
13500
  proxyMode: proxyMode, // Use prop value
13287
13501
  ngrokSkipBrowserWarning: true,
13288
13502
  logger: logger, // Pass logger to SDK for verbose SDK logging
13503
+ persistToken: false, // AuthKit's tokenStorage owns persistence (avoid double-writer race)
13289
13504
  });
13290
13505
  log.log('SDK reinitialized successfully');
13291
13506
  // Restore bearer token after reinitialization using auth.verifyToken (standalone mode only)
@@ -13315,11 +13530,34 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13315
13530
  // Get the full current URL including hash routes, strip query params
13316
13531
  return window.location.href.split('?')[0];
13317
13532
  };
13533
+ const syncWhatsAppResumeUrl = (pending) => {
13534
+ if (typeof window === 'undefined')
13535
+ return;
13536
+ try {
13537
+ const currentUrl = new URL(window.location.href);
13538
+ const isOnResumeUrl = currentUrl.searchParams.get('mode') === 'whatsapp';
13539
+ if (!pending) {
13540
+ if (!isOnResumeUrl)
13541
+ return;
13542
+ const cleanUrl = stripWhatsAppResumeParams(currentUrl.toString());
13543
+ window.history.replaceState({}, document.title, cleanUrl);
13544
+ return;
13545
+ }
13546
+ const resumedUrl = appendWhatsAppResumeParams(pending.redirectUrl || currentUrl.toString(), pending.token);
13547
+ if (currentUrl.toString() !== resumedUrl) {
13548
+ window.history.replaceState({}, document.title, resumedUrl);
13549
+ }
13550
+ }
13551
+ catch (err) {
13552
+ log.warn('Failed to sync WhatsApp resume URL state:', err);
13553
+ }
13554
+ };
13318
13555
  const savePendingWhatsAppSession = async (session) => {
13319
13556
  if (proxyMode)
13320
13557
  return;
13321
13558
  const storage = await getStorage();
13322
13559
  await storage.setItem(WHATSAPP_PENDING_SESSION_KEY, session);
13560
+ syncWhatsAppResumeUrl(session);
13323
13561
  };
13324
13562
  const loadPendingWhatsAppSession = async () => {
13325
13563
  if (proxyMode)
@@ -13332,6 +13570,17 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13332
13570
  return;
13333
13571
  const storage = await getStorage();
13334
13572
  await storage.removeItem(WHATSAPP_PENDING_SESSION_KEY);
13573
+ syncWhatsAppResumeUrl(null);
13574
+ };
13575
+ const updatePendingWhatsAppSession = async (updates) => {
13576
+ if (proxyMode)
13577
+ return null;
13578
+ const existing = await loadPendingWhatsAppSession();
13579
+ if (!existing)
13580
+ return null;
13581
+ const next = { ...existing, ...updates };
13582
+ await savePendingWhatsAppSession(next);
13583
+ return next;
13335
13584
  };
13336
13585
  // Fetch UI configuration
13337
13586
  React.useEffect(() => {
@@ -13505,6 +13754,12 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13505
13754
  // Google OAuth redirect callback
13506
13755
  handleGoogleAuthCodeCallback(authCode, state);
13507
13756
  }
13757
+ else if (urlMode === 'whatsapp') {
13758
+ if (!auth.user?.uid) {
13759
+ setMode('whatsapp');
13760
+ }
13761
+ setUrlAuthProcessing(false);
13762
+ }
13508
13763
  else if (urlMode && token) {
13509
13764
  handleURLBasedAuth(urlMode, token);
13510
13765
  }
@@ -14428,30 +14683,53 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
14428
14683
  let cancelled = false;
14429
14684
  const resumePendingWhatsAppSession = async () => {
14430
14685
  try {
14686
+ const urlParams = buildSearchParams(window.location.search);
14687
+ const resumeMode = urlParams.get('mode');
14688
+ const resumeToken = urlParams.get('token');
14431
14689
  const pending = await loadPendingWhatsAppSession();
14432
- if (!pending || cancelled)
14690
+ if (!pending || cancelled) {
14691
+ if (resumeMode === 'whatsapp') {
14692
+ syncWhatsAppResumeUrl(null);
14693
+ }
14433
14694
  return;
14695
+ }
14434
14696
  // Expire stale pending sessions after 30 minutes to avoid resurrecting old attempts.
14435
14697
  if (Date.now() - pending.createdAt > 30 * 60 * 1000) {
14436
14698
  await clearPendingWhatsAppSession();
14437
14699
  return;
14438
14700
  }
14701
+ if (resumeMode === 'whatsapp' && resumeToken && resumeToken !== pending.token) {
14702
+ await clearPendingWhatsAppSession();
14703
+ return;
14704
+ }
14705
+ syncWhatsAppResumeUrl(pending);
14439
14706
  whatsappSendRef.current = {
14440
14707
  token: pending.token,
14441
14708
  sessionKey: pending.sessionKey,
14442
14709
  displayName: pending.displayName,
14443
14710
  };
14711
+ setRestoredWhatsAppSend({
14712
+ waLink: pending.waLink,
14713
+ code: pending.code,
14714
+ token: pending.token,
14715
+ expiresAt: pending.expiresAt,
14716
+ sessionKey: pending.sessionKey,
14717
+ });
14444
14718
  const status = await api.getWhatsAppStatus(pending.token);
14445
14719
  if (cancelled)
14446
14720
  return;
14447
14721
  if (status.verified) {
14448
- await handleWhatsAppVerified(status);
14722
+ const handled = await handleWhatsAppVerified(status);
14723
+ if (handled === false && !cancelled) {
14724
+ setMode('whatsapp');
14725
+ }
14449
14726
  return;
14450
14727
  }
14451
- if (status.status === 'pending') {
14728
+ if (resumeMode === 'whatsapp' || status.status === 'pending') {
14452
14729
  setMode('whatsapp');
14453
14730
  }
14454
14731
  else if (status.status === 'failed' || status.status === 'expired' || status.status === 'unknown') {
14732
+ setRestoredWhatsAppSend(null);
14455
14733
  await clearPendingWhatsAppSession();
14456
14734
  }
14457
14735
  }
@@ -14492,8 +14770,12 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
14492
14770
  sessionKey: result.sessionKey,
14493
14771
  displayName: trimmedName,
14494
14772
  };
14773
+ setRestoredWhatsAppSend(result);
14495
14774
  await savePendingWhatsAppSession({
14775
+ waLink: result.waLink,
14776
+ code: result.code,
14496
14777
  token: result.token,
14778
+ expiresAt: result.expiresAt,
14497
14779
  sessionKey: result.sessionKey,
14498
14780
  displayName: trimmedName,
14499
14781
  redirectUrl: effectiveRedirectUrl,
@@ -14523,22 +14805,39 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
14523
14805
  const send = whatsappSendRef.current;
14524
14806
  if (send?.sessionKey) {
14525
14807
  try {
14808
+ await updatePendingWhatsAppSession({ exchangeStartedAt: Date.now() });
14526
14809
  const session = await api.exchangeWhatsAppSession(send.token, send.sessionKey);
14527
14810
  if (session?.token && session.user) {
14528
14811
  await auth.login(session.token, session.user, session.accountData, true, getExpirationFromResponse(session));
14529
14812
  if (!proxyMode) {
14530
14813
  onAuthSuccess(session.token, session.user, session.accountData);
14531
14814
  }
14815
+ await updatePendingWhatsAppSession({ exchangeCompletedAt: Date.now() });
14816
+ setRestoredWhatsAppSend(null);
14532
14817
  await clearPendingWhatsAppSession();
14533
- return;
14818
+ return true;
14534
14819
  }
14535
14820
  }
14536
14821
  catch (err) {
14537
- log.warn('WhatsApp session exchange failed, falling back to redirect-only flow:', err);
14822
+ const latestPending = await loadPendingWhatsAppSession();
14823
+ if (!latestPending) {
14824
+ return true;
14825
+ }
14826
+ const recentlyCompleted = latestPending.exchangeCompletedAt && (Date.now() - latestPending.exchangeCompletedAt) < 15000;
14827
+ if (recentlyCompleted) {
14828
+ return true;
14829
+ }
14830
+ log.warn('WhatsApp session exchange failed; keeping pending session for retry:', err);
14831
+ setAuthSuccess(false);
14832
+ setSuccessMessage(undefined);
14833
+ setError('WhatsApp verified, but finishing sign-in is taking longer than expected. Retrying…');
14834
+ return false;
14538
14835
  }
14539
14836
  }
14837
+ setRestoredWhatsAppSend(null);
14540
14838
  await clearPendingWhatsAppSession();
14541
14839
  performRedirect(target, 'magic-link');
14840
+ return true;
14542
14841
  };
14543
14842
  // Show processing state for URL-based auth (verification, magic link, password reset)
14544
14843
  // This runs BEFORE configLoading check to prevent form flash on deep-link flows
@@ -14548,8 +14847,9 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
14548
14847
  fontSize: '0.875rem'
14549
14848
  }, children: initialUrlParams.mode === 'verifyEmail' ? 'Verifying your email...' :
14550
14849
  initialUrlParams.mode === 'magicLink' ? 'Processing magic link...' :
14551
- initialUrlParams.mode === 'resetPassword' ? 'Validating reset link...' :
14552
- 'Processing...' })] }) }));
14850
+ initialUrlParams.mode === 'whatsapp' ? 'Resuming your WhatsApp sign-in...' :
14851
+ initialUrlParams.mode === 'resetPassword' ? 'Validating reset link...' :
14852
+ 'Processing...' })] }) }));
14553
14853
  }
14554
14854
  if (configLoading) {
14555
14855
  return (jsxRuntime.jsx(AuthContainer, { theme: resolvedTheme, className: className, minimal: minimal || config?.branding?.minimal || false, children: jsxRuntime.jsx("div", { style: { textAlign: 'center', padding: '2rem' }, children: jsxRuntime.jsx("div", { className: "auth-spinner" }) }) }));
@@ -14574,7 +14874,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
14574
14874
  ? 'hsl(var(--muted-foreground, 215 15% 45%))'
14575
14875
  : (resolvedTheme === 'dark' ? '#94a3b8' : '#6B7280'),
14576
14876
  fontSize: '0.875rem'
14577
- }, children: successMessage })] })) : mode === 'magic-link' ? (jsxRuntime.jsx(MagicLinkForm, { onSubmit: handleMagicLink, onCancel: () => setMode('login'), loading: loading, error: error, collectName: config?.collectNameOnPasswordlessSignup })) : mode === 'whatsapp' ? (jsxRuntime.jsx(WhatsAppAuthForm, { onSend: handleWhatsAppSend, onPollStatus: handleWhatsAppPoll, onVerified: handleWhatsAppVerified, onBack: () => setMode('login'), loading: loading, error: error, collectName: config?.collectNameOnPasswordlessSignup ?? true })) : mode === 'phone' ? (jsxRuntime.jsx(PhoneAuthForm, { onSubmit: handlePhoneAuth, onBack: () => setMode('login'), loading: loading, error: error, collectName: config?.collectNameOnPasswordlessSignup })) : mode === 'reset-password' ? (jsxRuntime.jsx(PasswordResetForm, { onSubmit: handlePasswordReset, onBack: () => {
14877
+ }, children: successMessage })] })) : mode === 'magic-link' ? (jsxRuntime.jsx(MagicLinkForm, { onSubmit: handleMagicLink, onCancel: () => setMode('login'), loading: loading, error: error, collectName: config?.collectNameOnPasswordlessSignup })) : mode === 'whatsapp' ? (jsxRuntime.jsx(WhatsAppAuthForm, { onSend: handleWhatsAppSend, onPollStatus: handleWhatsAppPoll, onVerified: handleWhatsAppVerified, onBack: () => setMode('login'), loading: loading, error: error, collectName: config?.collectNameOnPasswordlessSignup ?? true, initialSent: restoredWhatsAppSend })) : mode === 'phone' ? (jsxRuntime.jsx(PhoneAuthForm, { onSubmit: handlePhoneAuth, onBack: () => setMode('login'), loading: loading, error: error, collectName: config?.collectNameOnPasswordlessSignup })) : mode === 'reset-password' ? (jsxRuntime.jsx(PasswordResetForm, { onSubmit: handlePasswordReset, onBack: () => {
14578
14878
  setMode('login');
14579
14879
  setResetSuccess(false);
14580
14880
  setResetToken(undefined); // Clear token when going back
@@ -14925,6 +15225,7 @@ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, class
14925
15225
  baseURL: apiEndpoint,
14926
15226
  proxyMode: false,
14927
15227
  ngrokSkipBrowserWarning: true,
15228
+ persistToken: false, // AuthKit's tokenStorage is the single source of truth
14928
15229
  });
14929
15230
  }
14930
15231
  }, [apiEndpoint]);