UNPKG

@replyke/core

Version:

Replyke: Build interactive apps with social features like comments, votes, feeds, user lists, notifications, and more.

121 lines 5.21 kB
import { useEffect, useRef } from "react"; import { useReplykeDispatch, useReplykeSelector } from "../../store/hooks"; import { setAccountMap, upsertAccount, setActiveAccount, setAccountsReady, registerAccountManager, selectAccounts, selectActiveAccountId, selectAccountsReady, } from "../../store/slices/accountsSlice"; import { selectRefreshToken, setRefreshToken } from "../../store/slices/authSlice"; import { selectUser } from "../../store/slices/userSlice"; import { handleError } from "../../utils/handleError"; function base64UrlDecode(str) { // Convert base64url to standard base64 const base64 = str.replace(/-/g, "+").replace(/_/g, "/"); if (typeof atob === "function") return atob(base64); // Fallback for React Native (Buffer available via Node.js polyfill or hermes) const GlobalBuffer = globalThis.Buffer; if (typeof GlobalBuffer === "function") return GlobalBuffer.from(base64, "base64").toString("utf-8"); return ""; } function extractExpFromJwt(jwt) { try { const payload = JSON.parse(base64UrlDecode(jwt.split(".")[1])); return (payload.exp ?? 0) * 1000; } catch { return 0; } } export default function useAccountSync(storage, projectId) { const dispatch = useReplykeDispatch(); const refreshToken = useReplykeSelector(selectRefreshToken); const user = useReplykeSelector(selectUser); // from userSlice (canonical) const accounts = useReplykeSelector(selectAccounts); const activeAccountId = useReplykeSelector(selectActiveAccountId); const isReady = useReplykeSelector(selectAccountsReady); const isInitialLoadRef = useRef(true); // Phase A: Mount — register + load from storage useEffect(() => { dispatch(registerAccountManager()); const loadAccounts = async () => { try { const map = await storage.getAccountMap(projectId); if (map) { // If no active account is set (or it points to a removed account), // default to the first available account on load const accountIds = Object.keys(map.accounts); if (accountIds.length > 0 && (!map.activeAccountId || !map.accounts[map.activeAccountId])) { map.activeAccountId = accountIds[0]; } dispatch(setAccountMap(map)); if (map.activeAccountId && map.accounts[map.activeAccountId]) { dispatch(setRefreshToken(map.accounts[map.activeAccountId].refreshToken)); } } } catch (error) { handleError(error, "Failed to load account map from storage"); } finally { dispatch(setAccountsReady(true)); } }; loadAccounts(); }, []); // projectId is stable for lifetime of ReplykeProvider // Phase B: Watch refreshToken + user — upsert account entries useEffect(() => { if (!isReady || !refreshToken || !user?.id) return; const summary = { id: user.id, name: user.name ?? null, email: user.email ?? null, avatar: user.avatar ?? null, }; const entry = { refreshToken, tokenExpiresAt: extractExpFromJwt(refreshToken), user: summary, }; dispatch(upsertAccount({ userId: user.id, entry })); if (user.id !== activeAccountId) { dispatch(setActiveAccount(user.id)); } }, [refreshToken, user, isReady]); // Phase C: Persist map on changes useEffect(() => { if (!isReady) return; // Skip persisting the initial load (that data came FROM storage) if (isInitialLoadRef.current) { isInitialLoadRef.current = false; return; } const map = { activeAccountId, accounts }; storage.setAccountMap(projectId, map).catch((error) => { handleError(error, "Failed to persist account map"); }); }, [accounts, activeAccountId, isReady]); // Phase D: Cross-tab sync (web only) useEffect(() => { if (typeof window === "undefined") return; const storageKey = `replyke-accounts:${projectId}`; const handleStorageEvent = (event) => { if (event.key !== storageKey || !event.newValue) return; try { const map = JSON.parse(event.newValue); dispatch(setAccountMap(map)); if (map.activeAccountId && map.accounts[map.activeAccountId]) { dispatch(setRefreshToken(map.accounts[map.activeAccountId].refreshToken)); } } catch (error) { handleError(error, "Failed to sync account map from storage event"); } }; window.addEventListener("storage", handleStorageEvent); return () => window.removeEventListener("storage", handleStorageEvent); }, [projectId, dispatch]); } //# sourceMappingURL=useAccountSync.js.map