mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
185 lines
5.5 KiB
TypeScript
185 lines
5.5 KiB
TypeScript
import React, { useEffect, useState, useContext, useMemo } from "react";
|
||
import firebase from "firebase/app";
|
||
import createPersistedState from "use-persisted-state";
|
||
import _merge from "lodash/merge";
|
||
import Helmet from "react-helmet";
|
||
|
||
import { useMediaQuery, ThemeProvider, CssBaseline } from "@mui/material";
|
||
|
||
import ErrorBoundary from "components/ErrorBoundary";
|
||
|
||
import { projectId, auth, db } from "@src/firebase";
|
||
import useDoc from "hooks/useDoc";
|
||
import { name } from "@root/package.json";
|
||
import { PUBLIC_SETTINGS, USERS } from "config/dbPaths";
|
||
import { analytics } from "analytics";
|
||
import themes from "theme";
|
||
|
||
const useThemeState = createPersistedState("__ROWY__THEME");
|
||
const useThemeOverriddenState = createPersistedState(
|
||
"__ROWY__THEME_OVERRIDDEN"
|
||
);
|
||
|
||
interface IAppContext {
|
||
projectId: string;
|
||
currentUser: firebase.User | null | undefined;
|
||
userClaims: Record<string, any> | undefined;
|
||
userRoles: null | string[];
|
||
authToken: string;
|
||
userDoc: any;
|
||
theme: keyof typeof themes;
|
||
themeOverridden: boolean;
|
||
setTheme: React.Dispatch<React.SetStateAction<keyof typeof themes>>;
|
||
setThemeOverridden: React.Dispatch<React.SetStateAction<boolean>>;
|
||
}
|
||
|
||
export const AppContext = React.createContext<IAppContext>({
|
||
projectId: "",
|
||
currentUser: undefined,
|
||
userClaims: undefined,
|
||
userRoles: [],
|
||
authToken: "",
|
||
userDoc: undefined,
|
||
theme: "light",
|
||
themeOverridden: false,
|
||
setTheme: () => {},
|
||
setThemeOverridden: () => {},
|
||
});
|
||
|
||
export const useAppContext = () => useContext(AppContext);
|
||
|
||
export const AppProvider: React.FC = ({ children }) => {
|
||
// Store currentUser from Firebase Auth
|
||
const [currentUser, setCurrentUser] = useState<
|
||
firebase.User | null | undefined
|
||
>();
|
||
// Store user auth data
|
||
const [userClaims, setUserClaims] =
|
||
useState<IAppContext["userClaims"]>(undefined);
|
||
const [userRoles, setUserRoles] = useState<IAppContext["userRoles"]>([]);
|
||
const [authToken, setAuthToken] = useState<IAppContext["authToken"]>("");
|
||
|
||
// Get user data from Firebase Auth event
|
||
useEffect(() => {
|
||
auth.onAuthStateChanged((auth) => {
|
||
setCurrentUser(auth);
|
||
|
||
if (auth)
|
||
auth.getIdTokenResult(true).then((results) => {
|
||
setAuthToken(results.token);
|
||
setUserRoles(
|
||
Array.isArray(results.claims.roles) ? results.claims.roles : []
|
||
);
|
||
setUserClaims(results.claims);
|
||
});
|
||
});
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
document.title = `${projectId} • ${name}`;
|
||
}, []);
|
||
|
||
const [publicSettings] = useDoc(
|
||
{ path: PUBLIC_SETTINGS },
|
||
{ createIfMissing: true }
|
||
);
|
||
|
||
// Store matching userDoc
|
||
const [userDoc, dispatchUserDoc] = useDoc({}, { createIfMissing: true });
|
||
// Get userDoc
|
||
useEffect(() => {
|
||
if (currentUser) {
|
||
analytics.setUserId(currentUser.uid);
|
||
analytics.setUserProperties({ instance: window.location.hostname });
|
||
dispatchUserDoc({ path: `${USERS}/${currentUser.uid}` });
|
||
}
|
||
}, [currentUser]);
|
||
|
||
// Set userDoc if it doesn’t exist
|
||
useEffect(() => {
|
||
if (
|
||
(!userDoc.doc || !userDoc.doc.user) &&
|
||
!userDoc.loading &&
|
||
userDoc.path &&
|
||
currentUser
|
||
) {
|
||
const userFields = ["email", "displayName", "photoURL", "phoneNumber"];
|
||
const user = userFields.reduce((acc, curr) => {
|
||
if (currentUser[curr]) return { ...acc, [curr]: currentUser[curr] };
|
||
return acc;
|
||
}, {});
|
||
console.log("create user", userDoc.path, user);
|
||
db.doc(userDoc.path).set({ user }, { merge: true });
|
||
}
|
||
}, [userDoc, currentUser]);
|
||
|
||
// Infer theme based on system settings
|
||
const prefersDarkTheme = useMediaQuery("(prefers-color-scheme: dark)", {
|
||
noSsr: true,
|
||
});
|
||
// Store theme
|
||
const [theme, setTheme] = useThemeState<keyof typeof themes>(
|
||
prefersDarkTheme ? "dark" : "light"
|
||
);
|
||
// Store if theme was overridden
|
||
const [themeOverridden, setThemeOverridden] = useThemeOverriddenState(false);
|
||
// Update theme when system settings change
|
||
useEffect(() => {
|
||
if (themeOverridden) return;
|
||
if (prefersDarkTheme && theme !== "dark") setTheme("dark");
|
||
if (!prefersDarkTheme && theme !== "light") setTheme("light");
|
||
}, [prefersDarkTheme, themeOverridden]);
|
||
// Customize theme from project public settings & user settings
|
||
const customizedThemes = useMemo(() => {
|
||
const lightCustomizations = _merge(
|
||
{},
|
||
publicSettings.doc?.theme?.base,
|
||
publicSettings.doc?.theme?.light,
|
||
userDoc.doc?.theme?.base,
|
||
userDoc.doc?.theme?.light
|
||
);
|
||
const darkCustomizations = _merge(
|
||
{},
|
||
publicSettings.doc?.theme?.base,
|
||
publicSettings.doc?.theme?.dark,
|
||
userDoc.doc?.theme?.base,
|
||
userDoc.doc?.theme?.dark
|
||
);
|
||
|
||
return {
|
||
light: themes.light(lightCustomizations),
|
||
dark: themes.dark(darkCustomizations),
|
||
};
|
||
}, [userDoc.doc, publicSettings.doc]);
|
||
|
||
return (
|
||
<AppContext.Provider
|
||
value={{
|
||
projectId,
|
||
currentUser,
|
||
userClaims,
|
||
userRoles,
|
||
authToken,
|
||
userDoc: { state: userDoc, dispatch: dispatchUserDoc },
|
||
theme,
|
||
themeOverridden,
|
||
setTheme,
|
||
setThemeOverridden,
|
||
}}
|
||
>
|
||
{Array.isArray(customizedThemes[theme].typography.fontCssUrls) && (
|
||
<Helmet>
|
||
{customizedThemes[theme].typography.fontCssUrls!.map((url) => (
|
||
<link key={url} rel="stylesheet" href={url} />
|
||
))}
|
||
</Helmet>
|
||
)}
|
||
|
||
<ThemeProvider theme={customizedThemes[theme]}>
|
||
<CssBaseline />
|
||
<ErrorBoundary>{children}</ErrorBoundary>
|
||
</ThemeProvider>
|
||
</AppContext.Provider>
|
||
);
|
||
};
|