UNPKG

@replyke/core

Version:

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

124 lines 5.55 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = useAccountSync; const react_1 = require("react"); const hooks_1 = require("../../store/hooks"); const accountsSlice_1 = require("../../store/slices/accountsSlice"); const authSlice_1 = require("../../store/slices/authSlice"); const userSlice_1 = require("../../store/slices/userSlice"); const handleError_1 = require("../../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; } } function useAccountSync(storage, projectId) { const dispatch = (0, hooks_1.useReplykeDispatch)(); const refreshToken = (0, hooks_1.useReplykeSelector)(authSlice_1.selectRefreshToken); const user = (0, hooks_1.useReplykeSelector)(userSlice_1.selectUser); // from userSlice (canonical) const accounts = (0, hooks_1.useReplykeSelector)(accountsSlice_1.selectAccounts); const activeAccountId = (0, hooks_1.useReplykeSelector)(accountsSlice_1.selectActiveAccountId); const isReady = (0, hooks_1.useReplykeSelector)(accountsSlice_1.selectAccountsReady); const isInitialLoadRef = (0, react_1.useRef)(true); // Phase A: Mount — register + load from storage (0, react_1.useEffect)(() => { dispatch((0, accountsSlice_1.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((0, accountsSlice_1.setAccountMap)(map)); if (map.activeAccountId && map.accounts[map.activeAccountId]) { dispatch((0, authSlice_1.setRefreshToken)(map.accounts[map.activeAccountId].refreshToken)); } } } catch (error) { (0, handleError_1.handleError)(error, "Failed to load account map from storage"); } finally { dispatch((0, accountsSlice_1.setAccountsReady)(true)); } }; loadAccounts(); }, []); // projectId is stable for lifetime of ReplykeProvider // Phase B: Watch refreshToken + user — upsert account entries (0, react_1.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((0, accountsSlice_1.upsertAccount)({ userId: user.id, entry })); if (user.id !== activeAccountId) { dispatch((0, accountsSlice_1.setActiveAccount)(user.id)); } }, [refreshToken, user, isReady]); // Phase C: Persist map on changes (0, react_1.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) => { (0, handleError_1.handleError)(error, "Failed to persist account map"); }); }, [accounts, activeAccountId, isReady]); // Phase D: Cross-tab sync (web only) (0, react_1.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((0, accountsSlice_1.setAccountMap)(map)); if (map.activeAccountId && map.accounts[map.activeAccountId]) { dispatch((0, authSlice_1.setRefreshToken)(map.accounts[map.activeAccountId].refreshToken)); } } catch (error) { (0, handleError_1.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