@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/components/AccountManagement.d.ts.map +1 -1
- package/dist/components/SmartlinksAuthUI.d.ts.map +1 -1
- package/dist/components/WhatsAppAuthForm.d.ts +4 -2
- package/dist/components/WhatsAppAuthForm.d.ts.map +1 -1
- package/dist/context/AuthContext.d.ts.map +1 -1
- package/dist/index.esm.js +320 -19
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +320 -19
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
10957
|
-
|
|
10958
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 === '
|
|
14552
|
-
'
|
|
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]);
|