@proveanything/smartlinks-auth-ui 0.5.11 → 0.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"AccountManagement.d.ts","sourceRoot":"","sources":["../../src/components/AccountManagement.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2C,MAAM,OAAO,CAAC;AAOhE,OAAO,KAAK,EAAE,sBAAsB,EAAe,MAAM,UAAU,CAAC;AACpE,OAAO,qBAAqB,CAAC;AAK7B,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAgzB9D,CAAC"}
1
+ {"version":3,"file":"AccountManagement.d.ts","sourceRoot":"","sources":["../../src/components/AccountManagement.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2C,MAAM,OAAO,CAAC;AAOhE,OAAO,KAAK,EAAE,sBAAsB,EAAe,MAAM,UAAU,CAAC;AACpE,OAAO,qBAAqB,CAAC;AAK7B,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAizB9D,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"SmartlinksAuthUI.d.ts","sourceRoot":"","sources":["../../src/components/SmartlinksAuthUI.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+C,MAAM,OAAO,CAAC;AAcpE,OAAO,KAAK,EAAE,qBAAqB,EAAyF,MAAM,UAAU,CAAC;AAiR7I,QAAA,MAAM,mBAAmB,QAAa,OAAO,CAAC,IAAI,CAqBjD,CAAC;AAqDF,OAAO,EAAE,mBAAmB,EAAE,CAAC;AAI/B,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CA2kE5D,CAAC"}
1
+ {"version":3,"file":"SmartlinksAuthUI.d.ts","sourceRoot":"","sources":["../../src/components/SmartlinksAuthUI.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+C,MAAM,OAAO,CAAC;AAcpE,OAAO,KAAK,EAAE,qBAAqB,EAAyF,MAAM,UAAU,CAAC;AAiR7I,QAAA,MAAM,mBAAmB,QAAa,OAAO,CAAC,IAAI,CAqBjD,CAAC;AAqDF,OAAO,EAAE,mBAAmB,EAAE,CAAC;AAI/B,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CA+nE5D,CAAC"}
@@ -17,8 +17,8 @@ interface WhatsAppAuthFormProps {
17
17
  onSend: (displayName?: string) => Promise<WhatsAppSendResult>;
18
18
  /** Poll verification status for the issued token. */
19
19
  onPollStatus: (token: string) => Promise<WhatsAppStatusResult>;
20
- /** Called once verification is confirmed (status === 'verified'). */
21
- onVerified: (result: WhatsAppStatusResult) => void;
20
+ /** Called once verification is confirmed (status === 'verified'). Return false to keep retrying. */
21
+ onVerified: (result: WhatsAppStatusResult) => void | Promise<boolean | void>;
22
22
  onBack: () => void;
23
23
  loading?: boolean;
24
24
  error?: string;
@@ -26,6 +26,8 @@ interface WhatsAppAuthFormProps {
26
26
  collectName?: boolean;
27
27
  /** Polling interval in ms (default 3000). */
28
28
  pollIntervalMs?: number;
29
+ /** Existing in-progress WhatsApp session restored after app switching/reload. */
30
+ initialSent?: WhatsAppSendResult | null;
29
31
  }
30
32
  export declare const WhatsAppAuthForm: React.FC<WhatsAppAuthFormProps>;
31
33
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"WhatsAppAuthForm.d.ts","sourceRoot":"","sources":["../../src/components/WhatsAppAuthForm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAC3D,OAAO,gBAAgB,CAAC;AAExB,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAC;IAClE,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,UAAU,qBAAqB;IAC7B,kGAAkG;IAClG,MAAM,EAAE,CAAC,WAAW,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC9D,qDAAqD;IACrD,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC/D,qEAAqE;IACrE,UAAU,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACnD,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAOD,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CAqS5D,CAAC"}
1
+ {"version":3,"file":"WhatsAppAuthForm.d.ts","sourceRoot":"","sources":["../../src/components/WhatsAppAuthForm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAC3D,OAAO,gBAAgB,CAAC;AAExB,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAC;IAClE,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,UAAU,qBAAqB;IAC7B,kGAAkG;IAClG,MAAM,EAAE,CAAC,WAAW,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC9D,qDAAqD;IACrD,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC/D,oGAAoG;IACpG,UAAU,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAC7E,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iFAAiF;IACjF,WAAW,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACzC;AAOD,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CAmV5D,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"AuthContext.d.ts","sourceRoot":"","sources":["../../src/context/AuthContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8E,MAAM,OAAO,CAAC;AAOnG,OAAO,KAAK,EAAqC,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGvG,eAAO,MAAM,WAAW,6CAAyD,CAAC;AAGlF,YAAY,EAAE,gBAAgB,EAAE,CAAC;AAEjC,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAm+BpD,CAAC;AAEF,eAAO,MAAM,OAAO,QAAO,gBAM1B,CAAC"}
1
+ {"version":3,"file":"AuthContext.d.ts","sourceRoot":"","sources":["../../src/context/AuthContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8E,MAAM,OAAO,CAAC;AAOnG,OAAO,KAAK,EAAqC,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGvG,eAAO,MAAM,WAAW,6CAAyD,CAAC;AAGlF,YAAY,EAAE,gBAAgB,EAAE,CAAC;AAEjC,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CA0oCpD,CAAC;AAEF,eAAO,MAAM,OAAO,QAAO,gBAM1B,CAAC"}
package/dist/index.esm.js CHANGED
@@ -10873,18 +10873,19 @@ const isDesktop = () => {
10873
10873
  return false;
10874
10874
  return !/Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent);
10875
10875
  };
10876
- const WhatsAppAuthForm = ({ onSend, onPollStatus, onVerified, onBack, loading = false, error, collectName = false, pollIntervalMs = 3000, }) => {
10876
+ const WhatsAppAuthForm = ({ onSend, onPollStatus, onVerified, onBack, loading = false, error, collectName = false, pollIntervalMs = 3000, initialSent = null, }) => {
10877
10877
  const [displayName, setDisplayName] = useState('');
10878
- const [sent, setSent] = useState(null);
10878
+ const [sent, setSent] = useState(initialSent ?? null);
10879
10879
  const [pollError, setPollError] = useState();
10880
10880
  const [qrDataUrl, setQrDataUrl] = useState(null);
10881
10881
  const [showOnDesktop] = useState(isDesktop());
10882
10882
  const verifiedFiredRef = useRef(false);
10883
10883
  const autoSentRef = useRef(false);
10884
+ const hydratedInitialTokenRef = useRef(initialSent?.token ?? null);
10884
10885
  // Auto-issue the WhatsApp link on mount when we don't need to collect a name.
10885
10886
  // When collectName is true, wait for the user to submit the name first.
10886
10887
  useEffect(() => {
10887
- if (collectName)
10888
+ if (collectName || sent)
10888
10889
  return;
10889
10890
  if (autoSentRef.current)
10890
10891
  return;
@@ -10892,13 +10893,24 @@ const WhatsAppAuthForm = ({ onSend, onPollStatus, onVerified, onBack, loading =
10892
10893
  (async () => {
10893
10894
  try {
10894
10895
  const result = await onSend();
10896
+ hydratedInitialTokenRef.current = result.token;
10895
10897
  setSent(result);
10896
10898
  }
10897
10899
  catch {
10898
10900
  // Surfaced via parent error prop
10899
10901
  }
10900
10902
  })();
10901
- }, [collectName, onSend]);
10903
+ }, [collectName, onSend, sent]);
10904
+ useEffect(() => {
10905
+ if (!initialSent?.token)
10906
+ return;
10907
+ if (initialSent.token === hydratedInitialTokenRef.current)
10908
+ return;
10909
+ hydratedInitialTokenRef.current = initialSent.token;
10910
+ verifiedFiredRef.current = false;
10911
+ setPollError(undefined);
10912
+ setSent(initialSent);
10913
+ }, [initialSent]);
10902
10914
  // Generate QR code for the WhatsApp deep-link (great for desktop scan-with-phone UX)
10903
10915
  useEffect(() => {
10904
10916
  if (!sent?.waLink)
@@ -10927,15 +10939,26 @@ const WhatsAppAuthForm = ({ onSend, onPollStatus, onVerified, onBack, loading =
10927
10939
  return;
10928
10940
  let stopped = false;
10929
10941
  let timer;
10942
+ let pollInFlight = false;
10943
+ const scheduleNext = () => {
10944
+ if (stopped)
10945
+ return;
10946
+ timer = setTimeout(tick, pollIntervalMs);
10947
+ };
10930
10948
  const tick = async () => {
10949
+ if (pollInFlight)
10950
+ return;
10951
+ pollInFlight = true;
10931
10952
  try {
10932
10953
  const status = await onPollStatus(sent.token);
10933
10954
  if (stopped)
10934
10955
  return;
10935
10956
  if (status.verified && !verifiedFiredRef.current) {
10936
- verifiedFiredRef.current = true;
10937
- onVerified(status);
10938
- return;
10957
+ const handled = await onVerified(status);
10958
+ if (handled !== false) {
10959
+ verifiedFiredRef.current = true;
10960
+ return;
10961
+ }
10939
10962
  }
10940
10963
  if (status.status === 'failed' || status.status === 'expired') {
10941
10964
  setPollError(status.status === 'expired'
@@ -10947,12 +10970,31 @@ const WhatsAppAuthForm = ({ onSend, onPollStatus, onVerified, onBack, loading =
10947
10970
  catch {
10948
10971
  // Transient errors — keep polling
10949
10972
  }
10950
- timer = setTimeout(tick, pollIntervalMs);
10973
+ finally {
10974
+ pollInFlight = false;
10975
+ }
10976
+ scheduleNext();
10977
+ };
10978
+ const handleResume = () => {
10979
+ if (document.visibilityState && document.visibilityState !== 'visible')
10980
+ return;
10981
+ tick().catch?.(() => { });
10982
+ };
10983
+ const handleVisibilityChange = () => {
10984
+ if (document.visibilityState === 'visible') {
10985
+ handleResume();
10986
+ }
10951
10987
  };
10952
- timer = setTimeout(tick, pollIntervalMs);
10988
+ tick().catch?.(() => { });
10989
+ window.addEventListener('focus', handleResume);
10990
+ window.addEventListener('pageshow', handleResume);
10991
+ document.addEventListener('visibilitychange', handleVisibilityChange);
10953
10992
  return () => {
10954
10993
  stopped = true;
10955
10994
  clearTimeout(timer);
10995
+ window.removeEventListener('focus', handleResume);
10996
+ window.removeEventListener('pageshow', handleResume);
10997
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
10956
10998
  };
10957
10999
  }, [sent?.token, onPollStatus, onVerified, pollIntervalMs]);
10958
11000
  const handleNameSubmit = async (e) => {
@@ -10961,6 +11003,7 @@ const WhatsAppAuthForm = ({ onSend, onPollStatus, onVerified, onBack, loading =
10961
11003
  verifiedFiredRef.current = false;
10962
11004
  try {
10963
11005
  const result = await onSend(displayName.trim() || undefined);
11006
+ hydratedInitialTokenRef.current = result.token;
10964
11007
  setSent(result);
10965
11008
  }
10966
11009
  catch {
@@ -10975,6 +11018,7 @@ const WhatsAppAuthForm = ({ onSend, onPollStatus, onVerified, onBack, loading =
10975
11018
  if (!collectName) {
10976
11019
  try {
10977
11020
  const result = await onSend();
11021
+ hydratedInitialTokenRef.current = result.token;
10978
11022
  setSent(result);
10979
11023
  }
10980
11024
  catch {
@@ -12537,6 +12581,140 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
12537
12581
  unsubscribe();
12538
12582
  };
12539
12583
  }, [proxyMode, notifyAuthStateChange]);
12584
+ // Mobile app-switching can miss BroadcastChannel/storage events, especially on iOS.
12585
+ // Re-read persisted auth state whenever the page becomes active again so a
12586
+ // WhatsApp-returned tab/window can pick up a session that another page wrote.
12587
+ useEffect(() => {
12588
+ if (proxyMode)
12589
+ return;
12590
+ const rehydrateFromStorage = async () => {
12591
+ try {
12592
+ const storedToken = await tokenStorage.getToken();
12593
+ const storedUser = await tokenStorage.getUser();
12594
+ const storedAccountData = await tokenStorage.getAccountData();
12595
+ const storedAccountInfo = await tokenStorage.getAccountInfo();
12596
+ const storedContactId = await tokenStorage.getContactId();
12597
+ if (!storedToken || !storedUser)
12598
+ return;
12599
+ const tokenChanged = storedToken.token !== token;
12600
+ const userChanged = storedUser.uid !== user?.uid;
12601
+ if (tokenChanged || userChanged || !user) {
12602
+ setToken(storedToken.token);
12603
+ setUser(storedUser);
12604
+ setAccountData(storedAccountData);
12605
+ setAccountInfo(storedAccountInfo?.data || storedAccountData || null);
12606
+ setContactId(storedContactId);
12607
+ setIsVerified(true);
12608
+ notifyAuthStateChange('SESSION_RESTORED', storedUser, storedToken.token, storedAccountData || null, storedAccountInfo?.data || storedAccountData || null, true, contact, storedContactId);
12609
+ }
12610
+ if (!isVerified || pendingVerificationRef.current) {
12611
+ smartlinks.auth.verifyToken(storedToken.token)
12612
+ .then(() => {
12613
+ setIsVerified(true);
12614
+ pendingVerificationRef.current = false;
12615
+ })
12616
+ .catch((error) => {
12617
+ if (!isNetworkError(error)) {
12618
+ console.warn('[AuthContext] Resume verification failed:', error);
12619
+ }
12620
+ });
12621
+ }
12622
+ }
12623
+ catch (error) {
12624
+ console.warn('[AuthContext] Failed to rehydrate auth on resume:', error);
12625
+ }
12626
+ };
12627
+ const handleResume = () => {
12628
+ if (typeof document !== 'undefined' && document.visibilityState !== 'visible')
12629
+ return;
12630
+ rehydrateFromStorage().catch(() => { });
12631
+ };
12632
+ const handleVisibilityChange = () => {
12633
+ if (document.visibilityState === 'visible') {
12634
+ handleResume();
12635
+ }
12636
+ };
12637
+ window.addEventListener('focus', handleResume);
12638
+ window.addEventListener('pageshow', handleResume);
12639
+ document.addEventListener('visibilitychange', handleVisibilityChange);
12640
+ return () => {
12641
+ window.removeEventListener('focus', handleResume);
12642
+ window.removeEventListener('pageshow', handleResume);
12643
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
12644
+ };
12645
+ }, [proxyMode, token, user, isVerified, notifyAuthStateChange, contact, isNetworkError]);
12646
+ // Mobile app-switching can also miss BOTH visibility/storage signals on the newly-opened
12647
+ // page (common with WhatsApp handoff on iOS/Android webviews). Keep a short-lived polling
12648
+ // window after mount while logged out so a session written by the background/original page
12649
+ // is still adopted without requiring a manual refresh.
12650
+ useEffect(() => {
12651
+ if (proxyMode || user || token)
12652
+ return;
12653
+ let cancelled = false;
12654
+ let intervalId = null;
12655
+ let timeoutId = null;
12656
+ let pollInFlight = false;
12657
+ const adoptStoredSession = async () => {
12658
+ if (cancelled || pollInFlight)
12659
+ return;
12660
+ pollInFlight = true;
12661
+ try {
12662
+ const storedToken = await tokenStorage.getToken();
12663
+ const storedUser = await tokenStorage.getUser();
12664
+ if (!storedToken?.token || !storedUser)
12665
+ return;
12666
+ const storedAccountData = await tokenStorage.getAccountData();
12667
+ const storedAccountInfo = await tokenStorage.getAccountInfo();
12668
+ const storedContactId = await tokenStorage.getContactId();
12669
+ if (cancelled)
12670
+ return;
12671
+ setToken(storedToken.token);
12672
+ setUser(storedUser);
12673
+ setAccountData(storedAccountData || null);
12674
+ setAccountInfo(storedAccountInfo?.data || storedAccountData || null);
12675
+ setContactId(storedContactId);
12676
+ setIsVerified(true);
12677
+ pendingVerificationRef.current = false;
12678
+ notifyAuthStateChange('SESSION_RESTORED', storedUser, storedToken.token, storedAccountData || null, storedAccountInfo?.data || storedAccountData || null, true, contact, storedContactId);
12679
+ smartlinks.auth.verifyToken(storedToken.token).catch((error) => {
12680
+ if (!isNetworkError(error)) {
12681
+ console.warn('[AuthContext] Background rehydrate verification failed:', error);
12682
+ }
12683
+ });
12684
+ if (intervalId) {
12685
+ clearInterval(intervalId);
12686
+ intervalId = null;
12687
+ }
12688
+ if (timeoutId) {
12689
+ clearTimeout(timeoutId);
12690
+ timeoutId = null;
12691
+ }
12692
+ }
12693
+ catch (error) {
12694
+ console.warn('[AuthContext] Failed to adopt stored session during mobile handoff:', error);
12695
+ }
12696
+ finally {
12697
+ pollInFlight = false;
12698
+ }
12699
+ };
12700
+ adoptStoredSession().catch(() => { });
12701
+ intervalId = setInterval(() => {
12702
+ adoptStoredSession().catch(() => { });
12703
+ }, 1000);
12704
+ timeoutId = setTimeout(() => {
12705
+ if (intervalId) {
12706
+ clearInterval(intervalId);
12707
+ intervalId = null;
12708
+ }
12709
+ }, 20000);
12710
+ return () => {
12711
+ cancelled = true;
12712
+ if (intervalId)
12713
+ clearInterval(intervalId);
12714
+ if (timeoutId)
12715
+ clearTimeout(timeoutId);
12716
+ };
12717
+ }, [proxyMode, user, token, notifyAuthStateChange, contact, isNetworkError]);
12540
12718
  // Helper: Send login to parent and wait for acknowledgment
12541
12719
  // Used for deep-link flows (email verification, magic link) where we need to ensure
12542
12720
  // the parent has persisted the session before redirecting (which causes page reload)
@@ -12570,10 +12748,12 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
12570
12748
  try {
12571
12749
  // Only persist to storage in standalone mode
12572
12750
  if (!proxyMode) {
12751
+ await tokenStorage.clearAccountInfo();
12573
12752
  await tokenStorage.saveToken(authToken, expiresAt);
12574
12753
  await tokenStorage.saveUser(authUser);
12575
12754
  if (authAccountData) {
12576
12755
  await tokenStorage.saveAccountData(authAccountData);
12756
+ await tokenStorage.saveAccountInfo(authAccountData, accountCacheTTL);
12577
12757
  }
12578
12758
  smartlinks.auth.verifyToken(authToken).catch(() => { });
12579
12759
  }
@@ -12581,6 +12761,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
12581
12761
  setToken(authToken);
12582
12762
  setUser(authUser);
12583
12763
  setAccountData(authAccountData || null);
12764
+ setAccountInfo(authAccountData || null);
12584
12765
  pendingVerificationRef.current = false;
12585
12766
  // Cross-iframe auth state synchronization
12586
12767
  // ALWAYS wait for parent acknowledgment in iframe mode before proceeding
@@ -12591,7 +12772,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
12591
12772
  }
12592
12773
  // NOW set isVerified - after parent has acknowledged and session is ready
12593
12774
  setIsVerified(true);
12594
- notifyAuthStateChange('LOGIN', authUser, authToken, authAccountData || null, null, true);
12775
+ notifyAuthStateChange('LOGIN', authUser, authToken, authAccountData || null, authAccountData || null, true);
12595
12776
  // Sync contact (non-blocking)
12596
12777
  const newContactId = await syncContact(authUser, authAccountData);
12597
12778
  // Track interaction (non-blocking)
@@ -12607,7 +12788,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
12607
12788
  console.error('Failed to save auth data to storage:', error);
12608
12789
  throw error;
12609
12790
  }
12610
- }, [proxyMode, notifyAuthStateChange, preloadAccountInfo, syncContact, trackInteraction]);
12791
+ }, [proxyMode, notifyAuthStateChange, preloadAccountInfo, syncContact, trackInteraction, accountCacheTTL]);
12611
12792
  const logout = useCallback(async () => {
12612
12793
  const currentUser = user;
12613
12794
  const currentContactId = contactId;
@@ -13203,6 +13384,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13203
13384
  const [silentSignInChecked, setSilentSignInChecked] = useState(false); // Track if silent sign-in has been checked
13204
13385
  const [googleFallbackToPopup, setGoogleFallbackToPopup] = useState(false); // Show popup fallback when FedCM is blocked/dismissed
13205
13386
  const [googleNativeTimedOut, setGoogleNativeTimedOut] = useState(false); // Native bridge callback timed out
13387
+ const [restoredWhatsAppSend, setRestoredWhatsAppSend] = useState(null);
13206
13388
  const log = useMemo(() => createLoggerWrapper(logger), [logger]);
13207
13389
  const api = new AuthAPI(apiEndpoint, clientId, clientName, logger);
13208
13390
  const auth = useAuth();
@@ -13266,6 +13448,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13266
13448
  proxyMode: proxyMode, // Use prop value
13267
13449
  ngrokSkipBrowserWarning: true,
13268
13450
  logger: logger, // Pass logger to SDK for verbose SDK logging
13451
+ persistToken: false, // AuthKit's tokenStorage owns persistence (avoid double-writer race)
13269
13452
  });
13270
13453
  log.log('SDK reinitialized successfully');
13271
13454
  // Restore bearer token after reinitialization using auth.verifyToken (standalone mode only)
@@ -13313,6 +13496,16 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13313
13496
  const storage = await getStorage();
13314
13497
  await storage.removeItem(WHATSAPP_PENDING_SESSION_KEY);
13315
13498
  };
13499
+ const updatePendingWhatsAppSession = async (updates) => {
13500
+ if (proxyMode)
13501
+ return null;
13502
+ const existing = await loadPendingWhatsAppSession();
13503
+ if (!existing)
13504
+ return null;
13505
+ const next = { ...existing, ...updates };
13506
+ await savePendingWhatsAppSession(next);
13507
+ return next;
13508
+ };
13316
13509
  // Fetch UI configuration
13317
13510
  useEffect(() => {
13318
13511
  // Wait for SDK to be ready before fetching config
@@ -14421,17 +14614,28 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
14421
14614
  sessionKey: pending.sessionKey,
14422
14615
  displayName: pending.displayName,
14423
14616
  };
14617
+ setRestoredWhatsAppSend({
14618
+ waLink: pending.waLink,
14619
+ code: pending.code,
14620
+ token: pending.token,
14621
+ expiresAt: pending.expiresAt,
14622
+ sessionKey: pending.sessionKey,
14623
+ });
14424
14624
  const status = await api.getWhatsAppStatus(pending.token);
14425
14625
  if (cancelled)
14426
14626
  return;
14427
14627
  if (status.verified) {
14428
- await handleWhatsAppVerified(status);
14628
+ const handled = await handleWhatsAppVerified(status);
14629
+ if (handled === false && !cancelled) {
14630
+ setMode('whatsapp');
14631
+ }
14429
14632
  return;
14430
14633
  }
14431
14634
  if (status.status === 'pending') {
14432
14635
  setMode('whatsapp');
14433
14636
  }
14434
14637
  else if (status.status === 'failed' || status.status === 'expired' || status.status === 'unknown') {
14638
+ setRestoredWhatsAppSend(null);
14435
14639
  await clearPendingWhatsAppSession();
14436
14640
  }
14437
14641
  }
@@ -14472,8 +14676,12 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
14472
14676
  sessionKey: result.sessionKey,
14473
14677
  displayName: trimmedName,
14474
14678
  };
14679
+ setRestoredWhatsAppSend(result);
14475
14680
  await savePendingWhatsAppSession({
14681
+ waLink: result.waLink,
14682
+ code: result.code,
14476
14683
  token: result.token,
14684
+ expiresAt: result.expiresAt,
14477
14685
  sessionKey: result.sessionKey,
14478
14686
  displayName: trimmedName,
14479
14687
  redirectUrl: effectiveRedirectUrl,
@@ -14503,22 +14711,39 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
14503
14711
  const send = whatsappSendRef.current;
14504
14712
  if (send?.sessionKey) {
14505
14713
  try {
14714
+ await updatePendingWhatsAppSession({ exchangeStartedAt: Date.now() });
14506
14715
  const session = await api.exchangeWhatsAppSession(send.token, send.sessionKey);
14507
14716
  if (session?.token && session.user) {
14508
14717
  await auth.login(session.token, session.user, session.accountData, true, getExpirationFromResponse(session));
14509
14718
  if (!proxyMode) {
14510
14719
  onAuthSuccess(session.token, session.user, session.accountData);
14511
14720
  }
14721
+ await updatePendingWhatsAppSession({ exchangeCompletedAt: Date.now() });
14722
+ setRestoredWhatsAppSend(null);
14512
14723
  await clearPendingWhatsAppSession();
14513
- return;
14724
+ return true;
14514
14725
  }
14515
14726
  }
14516
14727
  catch (err) {
14517
- log.warn('WhatsApp session exchange failed, falling back to redirect-only flow:', err);
14728
+ const latestPending = await loadPendingWhatsAppSession();
14729
+ if (!latestPending) {
14730
+ return true;
14731
+ }
14732
+ const recentlyCompleted = latestPending.exchangeCompletedAt && (Date.now() - latestPending.exchangeCompletedAt) < 15000;
14733
+ if (recentlyCompleted) {
14734
+ return true;
14735
+ }
14736
+ log.warn('WhatsApp session exchange failed; keeping pending session for retry:', err);
14737
+ setAuthSuccess(false);
14738
+ setSuccessMessage(undefined);
14739
+ setError('WhatsApp verified, but finishing sign-in is taking longer than expected. Retrying…');
14740
+ return false;
14518
14741
  }
14519
14742
  }
14743
+ setRestoredWhatsAppSend(null);
14520
14744
  await clearPendingWhatsAppSession();
14521
14745
  performRedirect(target, 'magic-link');
14746
+ return true;
14522
14747
  };
14523
14748
  // Show processing state for URL-based auth (verification, magic link, password reset)
14524
14749
  // This runs BEFORE configLoading check to prevent form flash on deep-link flows
@@ -14554,7 +14779,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
14554
14779
  ? 'hsl(var(--muted-foreground, 215 15% 45%))'
14555
14780
  : (resolvedTheme === 'dark' ? '#94a3b8' : '#6B7280'),
14556
14781
  fontSize: '0.875rem'
14557
- }, children: successMessage })] })) : mode === 'magic-link' ? (jsx(MagicLinkForm, { onSubmit: handleMagicLink, onCancel: () => setMode('login'), loading: loading, error: error, collectName: config?.collectNameOnPasswordlessSignup })) : mode === 'whatsapp' ? (jsx(WhatsAppAuthForm, { onSend: handleWhatsAppSend, onPollStatus: handleWhatsAppPoll, onVerified: handleWhatsAppVerified, onBack: () => setMode('login'), loading: loading, error: error, collectName: config?.collectNameOnPasswordlessSignup ?? true })) : mode === 'phone' ? (jsx(PhoneAuthForm, { onSubmit: handlePhoneAuth, onBack: () => setMode('login'), loading: loading, error: error, collectName: config?.collectNameOnPasswordlessSignup })) : mode === 'reset-password' ? (jsx(PasswordResetForm, { onSubmit: handlePasswordReset, onBack: () => {
14782
+ }, children: successMessage })] })) : mode === 'magic-link' ? (jsx(MagicLinkForm, { onSubmit: handleMagicLink, onCancel: () => setMode('login'), loading: loading, error: error, collectName: config?.collectNameOnPasswordlessSignup })) : mode === 'whatsapp' ? (jsx(WhatsAppAuthForm, { onSend: handleWhatsAppSend, onPollStatus: handleWhatsAppPoll, onVerified: handleWhatsAppVerified, onBack: () => setMode('login'), loading: loading, error: error, collectName: config?.collectNameOnPasswordlessSignup ?? true, initialSent: restoredWhatsAppSend })) : mode === 'phone' ? (jsx(PhoneAuthForm, { onSubmit: handlePhoneAuth, onBack: () => setMode('login'), loading: loading, error: error, collectName: config?.collectNameOnPasswordlessSignup })) : mode === 'reset-password' ? (jsx(PasswordResetForm, { onSubmit: handlePasswordReset, onBack: () => {
14558
14783
  setMode('login');
14559
14784
  setResetSuccess(false);
14560
14785
  setResetToken(undefined); // Clear token when going back
@@ -14905,6 +15130,7 @@ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, class
14905
15130
  baseURL: apiEndpoint,
14906
15131
  proxyMode: false,
14907
15132
  ngrokSkipBrowserWarning: true,
15133
+ persistToken: false, // AuthKit's tokenStorage is the single source of truth
14908
15134
  });
14909
15135
  }
14910
15136
  }, [apiEndpoint]);