Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/serious-elephants-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensembleui/react-framework": patch
---

fix stale ensemble.user in modal
3 changes: 2 additions & 1 deletion packages/framework/src/evaluate/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
createStorageApi,
screenStorageAtom,
} from "../hooks/useEnsembleStorage";
import { createUserApi } from "../hooks/useEnsembleUser";
import { DateFormatter } from "../date/dateFormatter";
import {
themeAtom,
Expand Down Expand Up @@ -117,7 +118,7 @@ export const createBindingAtom = <T = unknown>(
: undefined,
),
user: rawJsExpression.includes("ensemble.user")
? get(userAtom)
? createUserApi(() => get(userAtom))
: undefined,
env: rawJsExpression.includes("ensemble.env")
? get(envAtom)
Expand Down
17 changes: 8 additions & 9 deletions packages/framework/src/hooks/useCommandCallback.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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 }),
Expand All @@ -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,
Expand Down
30 changes: 23 additions & 7 deletions packages/framework/src/hooks/useEnsembleUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,16 +29,31 @@ export const useEnsembleUser = (): EnsembleUser & EnsembleUserBuffer => {

// ensure first render on direct loads sees latest value from sessionStorage
const sessionSnapshot = useMemo<EnsembleUser>(() => {
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];
},
};
};
24 changes: 22 additions & 2 deletions packages/framework/src/hooks/useScreenContext.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -10,7 +10,9 @@ import {
screenDataAtom,
screenModelAtom,
themeModelAtom,
userAtom,
} from "../state";
import { getUserFromStorage } from "../utils/userStorage";
import type {
ApplicationContextDefinition,
ScreenContextActions,
Expand Down Expand Up @@ -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 (
<>
<UserAtomMount />
{children}
</>
);
};

const UserAtomMount: React.FC = () => {
const [user] = useAtom(userAtom);

useEffect(() => {
// ensure userAtom is mounted in the modal's store
}, [user]);

return null;
};

/**
Expand Down
1 change: 1 addition & 0 deletions packages/framework/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from "./date";
export * from "./state";
export * from "./evaluate";
export * from "./appConfig";
export * from "./utils/userStorage";
7 changes: 6 additions & 1 deletion packages/framework/src/state/user.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -60,7 +61,11 @@ const atomWithSessionStorage = <T = unknown>(
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 }));
Expand Down
16 changes: 16 additions & 0 deletions packages/framework/src/utils/userStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const getUserFromStorage = <T = unknown>(): 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
}
};