diff --git a/.changeset/serious-elephants-bathe.md b/.changeset/serious-elephants-bathe.md new file mode 100644 index 000000000..a2f882731 --- /dev/null +++ b/.changeset/serious-elephants-bathe.md @@ -0,0 +1,5 @@ +--- +"@ensembleui/react-framework": patch +--- + +fix stale ensemble.user in modal diff --git a/packages/framework/src/evaluate/binding.ts b/packages/framework/src/evaluate/binding.ts index 323722a4b..242d45f88 100644 --- a/packages/framework/src/evaluate/binding.ts +++ b/packages/framework/src/evaluate/binding.ts @@ -8,6 +8,7 @@ import { createStorageApi, screenStorageAtom, } from "../hooks/useEnsembleStorage"; +import { createUserApi } from "../hooks/useEnsembleUser"; import { DateFormatter } from "../date/dateFormatter"; import { themeAtom, @@ -117,7 +118,7 @@ export const createBindingAtom = ( : undefined, ), user: rawJsExpression.includes("ensemble.user") - ? get(userAtom) + ? createUserApi(() => get(userAtom)) : undefined, env: rawJsExpression.includes("ensemble.env") ? get(envAtom) diff --git a/packages/framework/src/hooks/useCommandCallback.ts b/packages/framework/src/hooks/useCommandCallback.ts index 5de7a1614..8879e75d6 100644 --- a/packages/framework/src/hooks/useCommandCallback.ts +++ b/packages/framework/src/hooks/useCommandCallback.ts @@ -1,9 +1,8 @@ import { useAtomCallback } from "jotai/utils"; import type { FC, ReactNode } from "react"; import { useCallback } from "react"; -import { mapKeys, assign } from "lodash-es"; +import { mapKeys } from "lodash-es"; import { createEvaluationContext } from "../evaluate"; -import type { EnsembleUser } from "../state"; import { appAtom, screenAtom, themeAtom, userAtom } from "../state"; import type { EnsembleContext, EnsembleLocation } from "../shared/ensemble"; import { DateFormatter } from "../date"; @@ -32,6 +31,7 @@ import type { } from "../shared"; import { deviceAtom } from "./useDeviceObserver"; import { createStorageApi, screenStorageAtom } from "./useEnsembleStorage"; +import { createUserApi } from "./useEnsembleUser"; import { useCustomScope } from "./useCustomScope"; import { useLanguageScope } from "./useLanguageScope"; @@ -65,12 +65,16 @@ export const useCommandCallback = < const storage = get(screenStorageAtom); const device = get(deviceAtom); const theme = get(themeAtom); - const user = get(userAtom); const storageApi = createStorageApi(storage, (next) => set(screenStorageAtom, next), ); + const userApi = createUserApi( + () => get(userAtom), + (nextUser) => set(userAtom, nextUser), + ); + const customWidgets = applicationContext.application?.customWidgets.reduce( (acc, widget) => ({ ...acc, [widget.name]: widget }), @@ -82,12 +86,7 @@ export const useCommandCallback = < screenContext, ensemble: { setTheme: (name: string) => set(themeAtom, name), - user: { - ...user, - set: (userUpdate: EnsembleUser) => - set(userAtom, assign({}, user, userUpdate)), - setUser: (userUpdate: EnsembleUser) => set(userAtom, userUpdate), - }, + user: userApi, storage: storageApi, formatter: DateFormatter(), env: applicationContext.env, diff --git a/packages/framework/src/hooks/useEnsembleUser.ts b/packages/framework/src/hooks/useEnsembleUser.ts index 40381732e..e0326eaca 100644 --- a/packages/framework/src/hooks/useEnsembleUser.ts +++ b/packages/framework/src/hooks/useEnsembleUser.ts @@ -2,6 +2,7 @@ import { useAtom } from "jotai"; import { assign, isEmpty } from "lodash-es"; import { useMemo } from "react"; import { userAtom, type EnsembleUser } from "../state"; +import { getUserFromStorage } from "../utils/userStorage"; interface EnsembleUserBuffer { set: (items: { [key: string]: unknown }) => void; @@ -28,16 +29,31 @@ export const useEnsembleUser = (): EnsembleUser & EnsembleUserBuffer => { // ensure first render on direct loads sees latest value from sessionStorage const sessionSnapshot = useMemo(() => { - try { - const raw = sessionStorage.getItem("ensemble.user"); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return raw ? JSON.parse(raw) : {}; - } catch { - return {}; - } + return getUserFromStorage(); }, []); const effectiveUser = isEmpty(user) ? sessionSnapshot : user; return { ...storageBuffer, ...effectiveUser }; }; + +export const createUserApi = ( + getUser: () => EnsembleUser, + setUser?: (user: EnsembleUser) => void, +) => { + const currentUser = getUser(); + return { + ...currentUser, + set: (userUpdate: EnsembleUser): void => { + const user = getUser(); + const updatedUser = assign({}, user, userUpdate); + setUser?.(updatedUser); + }, + setUser: (userUpdate: EnsembleUser): void => { + setUser?.(userUpdate); + }, + get: (key: string): unknown => { + return getUser()[key]; + }, + }; +}; diff --git a/packages/framework/src/hooks/useScreenContext.tsx b/packages/framework/src/hooks/useScreenContext.tsx index b68d3995c..f8eaf5f3c 100644 --- a/packages/framework/src/hooks/useScreenContext.tsx +++ b/packages/framework/src/hooks/useScreenContext.tsx @@ -1,5 +1,5 @@ import { Provider, useAtom, useAtomValue, useSetAtom } from "jotai"; -import { useCallback, useContext } from "react"; +import { useCallback, useContext, useEffect } from "react"; import { clone, merge } from "lodash-es"; import { useHydrateAtoms } from "jotai/utils"; import { @@ -10,7 +10,9 @@ import { screenDataAtom, screenModelAtom, themeModelAtom, + userAtom, } from "../state"; +import { getUserFromStorage } from "../utils/userStorage"; import type { ApplicationContextDefinition, ScreenContextActions, @@ -74,17 +76,35 @@ const HydrateAtoms: React.FC< > = ({ appContext, screenContext, children }) => { const themeScope = useContext(CustomThemeContext); + const latestUserData = getUserFromStorage(); + // initialising on state with prop on render here useHydrateAtoms([[screenAtom, screenContext]]); useHydrateAtoms([ [appAtom, appContext], [themeModelAtom, themeScope.theme], + [userAtom, latestUserData], ]); // initiate device resizer observer useDeviceObserver(); - return <>{children}; + return ( + <> + + {children} + + ); +}; + +const UserAtomMount: React.FC = () => { + const [user] = useAtom(userAtom); + + useEffect(() => { + // ensure userAtom is mounted in the modal's store + }, [user]); + + return null; }; /** diff --git a/packages/framework/src/index.ts b/packages/framework/src/index.ts index af7bf1098..d7a7ae5e0 100644 --- a/packages/framework/src/index.ts +++ b/packages/framework/src/index.ts @@ -10,3 +10,4 @@ export * from "./date"; export * from "./state"; export * from "./evaluate"; export * from "./appConfig"; +export * from "./utils/userStorage"; diff --git a/packages/framework/src/state/user.ts b/packages/framework/src/state/user.ts index dbd182719..a5b0dc409 100644 --- a/packages/framework/src/state/user.ts +++ b/packages/framework/src/state/user.ts @@ -1,5 +1,6 @@ import type { WritableAtom } from "jotai"; import { atom } from "jotai"; +import { setUserInStorage } from "../utils/userStorage"; export type EnsembleUser = { accessToken?: string } & { [key: string]: unknown; @@ -60,7 +61,11 @@ const atomWithSessionStorage = ( typeof update === "function" ? update(get(baseAtom)) : update ) as T; set(baseAtom, nextValue); - sessionStorage.setItem(key, JSON.stringify(nextValue)); + if (key === "ensemble.user") { + setUserInStorage(nextValue); + } else { + sessionStorage.setItem(key, JSON.stringify(nextValue)); + } // notify other stores in this tab to update immediately if (typeof window !== "undefined") { window.dispatchEvent(new StorageEvent("storage", { key })); diff --git a/packages/framework/src/utils/userStorage.ts b/packages/framework/src/utils/userStorage.ts new file mode 100644 index 000000000..62daa3a16 --- /dev/null +++ b/packages/framework/src/utils/userStorage.ts @@ -0,0 +1,16 @@ +export const getUserFromStorage = (): T => { + try { + const raw = sessionStorage.getItem("ensemble.user"); + return (raw ? JSON.parse(raw) : {}) as T; + } catch { + return {} as T; + } +}; + +export const setUserInStorage = (user: unknown): void => { + try { + sessionStorage.setItem("ensemble.user", JSON.stringify(user)); + } catch (err) { + // no-op + } +};