@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
|
@@ -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,
|
|
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;
|
|
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,
|
|
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,
|
|
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
|
-
|
|
10937
|
-
|
|
10938
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 === '
|
|
14532
|
-
'
|
|
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]);
|