@replyke/core
Version:
Replyke: Build interactive apps with social features like comments, votes, feeds, user lists, notifications, and more.
124 lines • 5.55 kB
JavaScript
;
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