@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.
@@ -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;AA6S7I,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,CA6qE5D,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,CA4oCpD,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,15 @@ 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);
12757
+ }
12758
+ else {
12759
+ await tokenStorage.clearAccountData();
12577
12760
  }
12578
12761
  smartlinks.auth.verifyToken(authToken).catch(() => { });
12579
12762
  }
@@ -12581,6 +12764,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
12581
12764
  setToken(authToken);
12582
12765
  setUser(authUser);
12583
12766
  setAccountData(authAccountData || null);
12767
+ setAccountInfo(authAccountData || null);
12584
12768
  pendingVerificationRef.current = false;
12585
12769
  // Cross-iframe auth state synchronization
12586
12770
  // ALWAYS wait for parent acknowledgment in iframe mode before proceeding
@@ -12591,7 +12775,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
12591
12775
  }
12592
12776
  // NOW set isVerified - after parent has acknowledged and session is ready
12593
12777
  setIsVerified(true);
12594
- notifyAuthStateChange('LOGIN', authUser, authToken, authAccountData || null, null, true);
12778
+ notifyAuthStateChange('LOGIN', authUser, authToken, authAccountData || null, authAccountData || null, true);
12595
12779
  // Sync contact (non-blocking)
12596
12780
  const newContactId = await syncContact(authUser, authAccountData);
12597
12781
  // Track interaction (non-blocking)
@@ -12607,7 +12791,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
12607
12791
  console.error('Failed to save auth data to storage:', error);
12608
12792
  throw error;
12609
12793
  }
12610
- }, [proxyMode, notifyAuthStateChange, preloadAccountInfo, syncContact, trackInteraction]);
12794
+ }, [proxyMode, notifyAuthStateChange, preloadAccountInfo, syncContact, trackInteraction, accountCacheTTL]);
12611
12795
  const logout = useCallback(async () => {
12612
12796
  const currentUser = user;
12613
12797
  const currentContactId = contactId;
@@ -12918,6 +13102,22 @@ const normalizeQueryString = (query) => {
12918
13102
  const buildSearchParams = (rawQuery) => {
12919
13103
  return new URLSearchParams(normalizeQueryString(rawQuery));
12920
13104
  };
13105
+ const appendWhatsAppResumeParams = (url, token) => {
13106
+ try {
13107
+ const nextUrl = new URL(url, window.location.origin);
13108
+ nextUrl.searchParams.set('mode', 'whatsapp');
13109
+ if (token) {
13110
+ nextUrl.searchParams.set('token', token);
13111
+ }
13112
+ else {
13113
+ nextUrl.searchParams.delete('token');
13114
+ }
13115
+ return nextUrl.toString();
13116
+ }
13117
+ catch {
13118
+ return url;
13119
+ }
13120
+ };
12921
13121
  // Helper to check for URL auth params synchronously (runs during initialization)
12922
13122
  // This prevents the form from flashing before detecting deep-link flows
12923
13123
  const getInitialUrlAuthParams = () => {
@@ -12940,6 +13140,19 @@ const getExpirationFromResponse = (response) => {
12940
13140
  return Date.now() + response.expiresIn;
12941
13141
  return undefined; // Will use 7-day default in tokenStorage
12942
13142
  };
13143
+ const stripWhatsAppResumeParams = (url) => {
13144
+ try {
13145
+ const urlObj = new URL(url);
13146
+ if (urlObj.searchParams.get('mode') === 'whatsapp') {
13147
+ urlObj.searchParams.delete('mode');
13148
+ urlObj.searchParams.delete('token');
13149
+ }
13150
+ return urlObj.toString();
13151
+ }
13152
+ catch {
13153
+ return url;
13154
+ }
13155
+ };
12943
13156
  const getActionResultErrorMessage = (result) => {
12944
13157
  if (!result || typeof result !== 'object')
12945
13158
  return null;
@@ -13203,6 +13416,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13203
13416
  const [silentSignInChecked, setSilentSignInChecked] = useState(false); // Track if silent sign-in has been checked
13204
13417
  const [googleFallbackToPopup, setGoogleFallbackToPopup] = useState(false); // Show popup fallback when FedCM is blocked/dismissed
13205
13418
  const [googleNativeTimedOut, setGoogleNativeTimedOut] = useState(false); // Native bridge callback timed out
13419
+ const [restoredWhatsAppSend, setRestoredWhatsAppSend] = useState(null);
13206
13420
  const log = useMemo(() => createLoggerWrapper(logger), [logger]);
13207
13421
  const api = new AuthAPI(apiEndpoint, clientId, clientName, logger);
13208
13422
  const auth = useAuth();
@@ -13266,6 +13480,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13266
13480
  proxyMode: proxyMode, // Use prop value
13267
13481
  ngrokSkipBrowserWarning: true,
13268
13482
  logger: logger, // Pass logger to SDK for verbose SDK logging
13483
+ persistToken: false, // AuthKit's tokenStorage owns persistence (avoid double-writer race)
13269
13484
  });
13270
13485
  log.log('SDK reinitialized successfully');
13271
13486
  // Restore bearer token after reinitialization using auth.verifyToken (standalone mode only)
@@ -13295,11 +13510,34 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13295
13510
  // Get the full current URL including hash routes, strip query params
13296
13511
  return window.location.href.split('?')[0];
13297
13512
  };
13513
+ const syncWhatsAppResumeUrl = (pending) => {
13514
+ if (typeof window === 'undefined')
13515
+ return;
13516
+ try {
13517
+ const currentUrl = new URL(window.location.href);
13518
+ const isOnResumeUrl = currentUrl.searchParams.get('mode') === 'whatsapp';
13519
+ if (!pending) {
13520
+ if (!isOnResumeUrl)
13521
+ return;
13522
+ const cleanUrl = stripWhatsAppResumeParams(currentUrl.toString());
13523
+ window.history.replaceState({}, document.title, cleanUrl);
13524
+ return;
13525
+ }
13526
+ const resumedUrl = appendWhatsAppResumeParams(pending.redirectUrl || currentUrl.toString(), pending.token);
13527
+ if (currentUrl.toString() !== resumedUrl) {
13528
+ window.history.replaceState({}, document.title, resumedUrl);
13529
+ }
13530
+ }
13531
+ catch (err) {
13532
+ log.warn('Failed to sync WhatsApp resume URL state:', err);
13533
+ }
13534
+ };
13298
13535
  const savePendingWhatsAppSession = async (session) => {
13299
13536
  if (proxyMode)
13300
13537
  return;
13301
13538
  const storage = await getStorage();
13302
13539
  await storage.setItem(WHATSAPP_PENDING_SESSION_KEY, session);
13540
+ syncWhatsAppResumeUrl(session);
13303
13541
  };
13304
13542
  const loadPendingWhatsAppSession = async () => {
13305
13543
  if (proxyMode)
@@ -13312,6 +13550,17 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13312
13550
  return;
13313
13551
  const storage = await getStorage();
13314
13552
  await storage.removeItem(WHATSAPP_PENDING_SESSION_KEY);
13553
+ syncWhatsAppResumeUrl(null);
13554
+ };
13555
+ const updatePendingWhatsAppSession = async (updates) => {
13556
+ if (proxyMode)
13557
+ return null;
13558
+ const existing = await loadPendingWhatsAppSession();
13559
+ if (!existing)
13560
+ return null;
13561
+ const next = { ...existing, ...updates };
13562
+ await savePendingWhatsAppSession(next);
13563
+ return next;
13315
13564
  };
13316
13565
  // Fetch UI configuration
13317
13566
  useEffect(() => {
@@ -13485,6 +13734,12 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
13485
13734
  // Google OAuth redirect callback
13486
13735
  handleGoogleAuthCodeCallback(authCode, state);
13487
13736
  }
13737
+ else if (urlMode === 'whatsapp') {
13738
+ if (!auth.user?.uid) {
13739
+ setMode('whatsapp');
13740
+ }
13741
+ setUrlAuthProcessing(false);
13742
+ }
13488
13743
  else if (urlMode && token) {
13489
13744
  handleURLBasedAuth(urlMode, token);
13490
13745
  }
@@ -14408,30 +14663,53 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
14408
14663
  let cancelled = false;
14409
14664
  const resumePendingWhatsAppSession = async () => {
14410
14665
  try {
14666
+ const urlParams = buildSearchParams(window.location.search);
14667
+ const resumeMode = urlParams.get('mode');
14668
+ const resumeToken = urlParams.get('token');
14411
14669
  const pending = await loadPendingWhatsAppSession();
14412
- if (!pending || cancelled)
14670
+ if (!pending || cancelled) {
14671
+ if (resumeMode === 'whatsapp') {
14672
+ syncWhatsAppResumeUrl(null);
14673
+ }
14413
14674
  return;
14675
+ }
14414
14676
  // Expire stale pending sessions after 30 minutes to avoid resurrecting old attempts.
14415
14677
  if (Date.now() - pending.createdAt > 30 * 60 * 1000) {
14416
14678
  await clearPendingWhatsAppSession();
14417
14679
  return;
14418
14680
  }
14681
+ if (resumeMode === 'whatsapp' && resumeToken && resumeToken !== pending.token) {
14682
+ await clearPendingWhatsAppSession();
14683
+ return;
14684
+ }
14685
+ syncWhatsAppResumeUrl(pending);
14419
14686
  whatsappSendRef.current = {
14420
14687
  token: pending.token,
14421
14688
  sessionKey: pending.sessionKey,
14422
14689
  displayName: pending.displayName,
14423
14690
  };
14691
+ setRestoredWhatsAppSend({
14692
+ waLink: pending.waLink,
14693
+ code: pending.code,
14694
+ token: pending.token,
14695
+ expiresAt: pending.expiresAt,
14696
+ sessionKey: pending.sessionKey,
14697
+ });
14424
14698
  const status = await api.getWhatsAppStatus(pending.token);
14425
14699
  if (cancelled)
14426
14700
  return;
14427
14701
  if (status.verified) {
14428
- await handleWhatsAppVerified(status);
14702
+ const handled = await handleWhatsAppVerified(status);
14703
+ if (handled === false && !cancelled) {
14704
+ setMode('whatsapp');
14705
+ }
14429
14706
  return;
14430
14707
  }
14431
- if (status.status === 'pending') {
14708
+ if (resumeMode === 'whatsapp' || status.status === 'pending') {
14432
14709
  setMode('whatsapp');
14433
14710
  }
14434
14711
  else if (status.status === 'failed' || status.status === 'expired' || status.status === 'unknown') {
14712
+ setRestoredWhatsAppSend(null);
14435
14713
  await clearPendingWhatsAppSession();
14436
14714
  }
14437
14715
  }
@@ -14472,8 +14750,12 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
14472
14750
  sessionKey: result.sessionKey,
14473
14751
  displayName: trimmedName,
14474
14752
  };
14753
+ setRestoredWhatsAppSend(result);
14475
14754
  await savePendingWhatsAppSession({
14755
+ waLink: result.waLink,
14756
+ code: result.code,
14476
14757
  token: result.token,
14758
+ expiresAt: result.expiresAt,
14477
14759
  sessionKey: result.sessionKey,
14478
14760
  displayName: trimmedName,
14479
14761
  redirectUrl: effectiveRedirectUrl,
@@ -14503,22 +14785,39 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
14503
14785
  const send = whatsappSendRef.current;
14504
14786
  if (send?.sessionKey) {
14505
14787
  try {
14788
+ await updatePendingWhatsAppSession({ exchangeStartedAt: Date.now() });
14506
14789
  const session = await api.exchangeWhatsAppSession(send.token, send.sessionKey);
14507
14790
  if (session?.token && session.user) {
14508
14791
  await auth.login(session.token, session.user, session.accountData, true, getExpirationFromResponse(session));
14509
14792
  if (!proxyMode) {
14510
14793
  onAuthSuccess(session.token, session.user, session.accountData);
14511
14794
  }
14795
+ await updatePendingWhatsAppSession({ exchangeCompletedAt: Date.now() });
14796
+ setRestoredWhatsAppSend(null);
14512
14797
  await clearPendingWhatsAppSession();
14513
- return;
14798
+ return true;
14514
14799
  }
14515
14800
  }
14516
14801
  catch (err) {
14517
- log.warn('WhatsApp session exchange failed, falling back to redirect-only flow:', err);
14802
+ const latestPending = await loadPendingWhatsAppSession();
14803
+ if (!latestPending) {
14804
+ return true;
14805
+ }
14806
+ const recentlyCompleted = latestPending.exchangeCompletedAt && (Date.now() - latestPending.exchangeCompletedAt) < 15000;
14807
+ if (recentlyCompleted) {
14808
+ return true;
14809
+ }
14810
+ log.warn('WhatsApp session exchange failed; keeping pending session for retry:', err);
14811
+ setAuthSuccess(false);
14812
+ setSuccessMessage(undefined);
14813
+ setError('WhatsApp verified, but finishing sign-in is taking longer than expected. Retrying…');
14814
+ return false;
14518
14815
  }
14519
14816
  }
14817
+ setRestoredWhatsAppSend(null);
14520
14818
  await clearPendingWhatsAppSession();
14521
14819
  performRedirect(target, 'magic-link');
14820
+ return true;
14522
14821
  };
14523
14822
  // Show processing state for URL-based auth (verification, magic link, password reset)
14524
14823
  // This runs BEFORE configLoading check to prevent form flash on deep-link flows
@@ -14528,8 +14827,9 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
14528
14827
  fontSize: '0.875rem'
14529
14828
  }, children: initialUrlParams.mode === 'verifyEmail' ? 'Verifying your email...' :
14530
14829
  initialUrlParams.mode === 'magicLink' ? 'Processing magic link...' :
14531
- initialUrlParams.mode === 'resetPassword' ? 'Validating reset link...' :
14532
- 'Processing...' })] }) }));
14830
+ initialUrlParams.mode === 'whatsapp' ? 'Resuming your WhatsApp sign-in...' :
14831
+ initialUrlParams.mode === 'resetPassword' ? 'Validating reset link...' :
14832
+ 'Processing...' })] }) }));
14533
14833
  }
14534
14834
  if (configLoading) {
14535
14835
  return (jsx(AuthContainer, { theme: resolvedTheme, className: className, minimal: minimal || config?.branding?.minimal || false, children: jsx("div", { style: { textAlign: 'center', padding: '2rem' }, children: jsx("div", { className: "auth-spinner" }) }) }));
@@ -14554,7 +14854,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
14554
14854
  ? 'hsl(var(--muted-foreground, 215 15% 45%))'
14555
14855
  : (resolvedTheme === 'dark' ? '#94a3b8' : '#6B7280'),
14556
14856
  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: () => {
14857
+ }, 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
14858
  setMode('login');
14559
14859
  setResetSuccess(false);
14560
14860
  setResetToken(undefined); // Clear token when going back
@@ -14905,6 +15205,7 @@ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, class
14905
15205
  baseURL: apiEndpoint,
14906
15206
  proxyMode: false,
14907
15207
  ngrokSkipBrowserWarning: true,
15208
+ persistToken: false, // AuthKit's tokenStorage is the single source of truth
14908
15209
  });
14909
15210
  }
14910
15211
  }, [apiEndpoint]);