import { useEffect } from "react"; import { refreshPlan } from "@app/services/api"; import { useSettings } from "@settings/contexts/SettingsContext"; import { mergeConfigListPatch } from "@app/services/utilities"; // SplashScreen's own post-connect reload (handleConnect) already forces a // fresh PHP render specifically to pick up a tier flip from the connect that // just happened. This hook's own self-heal check firing again on that same // freshly-reloaded mount is redundant - and if the platform's plan is still // settling in that narrow window (the same class of provisioning race // affecting other platform-resolved fields), it can detect its own `changed` // and fire a SECOND, unprompted reload before the user has done anything else. // flagSkipNextRefreshPlanReload() (called by SplashScreen right alongside // flagPollForResumeAfterReload, just before its own reload) suppresses // exactly that one redundant check - every navigation after this one keeps // self-healing normally. const SKIP_NEXT_REFRESH_PLAN_KEY = "sfsync_skip_next_refresh_plan"; export function flagSkipNextRefreshPlanReload(): void { try { sessionStorage.setItem(SKIP_NEXT_REFRESH_PLAN_KEY, "1"); } catch { // sessionStorage unavailable - nothing to flag; this hook's check just // runs as it did before this fix existed. } } function readAndClearSkipFlag(): boolean { try { const flagged = sessionStorage.getItem(SKIP_NEXT_REFRESH_PLAN_KEY) === "1"; if (flagged) { sessionStorage.removeItem(SKIP_NEXT_REFRESH_PLAN_KEY); } return flagged; } catch { return false; } } /** * Self-heal: re-validates the stored API key/plan against the platform and * corrects `features` in wp_options if it drifted (tampering, or a tier * change the platform already knows about but this session doesn't yet). * * Mounted once at the app root (App.tsx) rather than per-page - grid-manager, * not configuration, is the actual default landing page for onboarded users * (see Redirect.tsx), so a call scoped to the configuration route alone would * leave most sessions never running this check at all. `enabled` lets the * caller gate this on onboarded state without breaking the rules of hooks. * * `pathname` re-triggers the effect on every client-side navigation (App.tsx * itself never remounts when react-router swaps routes inside it) - matches * the old per-page useEffect's behavior of re-checking on every page visit, * on top of the once-per-load coverage the app-root hoist already added. */ export const useRefreshPlan = (enabled: boolean, pathname: string) => { const { setInitialSettings } = useSettings(); useEffect(() => { if (!enabled) return; if (readAndClearSkipFlag()) return; refreshPlan().then((result) => { if (result?.changed) { // A tier flip means the server-rendered sfsyncData this page was // built from is stale in more ways than just config_list - a full // reload already re-fetches everything, so merging here would be // redundant work discarded a moment later. window.location.reload(); return; } // Refreshes the connected account's name/username/biography/avatar // by polling instead of waiting on a pushed webhook (see SC-2839 - // the platform's own decision not to push). if (result?.config_list && Object.keys(result.config_list).length) { setInitialSettings((prev) => ({ ...prev, config_list: mergeConfigListPatch( prev.config_list, result.config_list ), })); } }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [enabled, pathname]); };