Skip to content

Commit

Permalink
fix: added global user state (#118)
Browse files Browse the repository at this point in the history
  • Loading branch information
amlan-roy committed Apr 16, 2024
1 parent 982cc1d commit c5a8438
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 10 deletions.
3 changes: 2 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { cn } from "@/lib/utils";
import { ThemeProvider } from "@/components/providers/components/theme-provider";
import { UserDataStoreProvider } from "@/components/providers/user-data-store-provider";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });
Expand All @@ -21,7 +22,7 @@ export default function RootLayout({
<html lang="en" suppressHydrationWarning={true}>
<body className={cn(inter.className, "min-h-screen flex flex-col")}>
<ThemeProvider attribute="class" enableSystem disableTransitionOnChange>
{children}
<UserDataStoreProvider>{children}</UserDataStoreProvider>
</ThemeProvider>
</body>
</html>
Expand Down
21 changes: 12 additions & 9 deletions src/components/global/AuthenticatedHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, { useEffect, useState } from "react";
import { sendEmailVerification } from "firebase/auth";
import { CircleUserRoundIcon } from "lucide-react";
import { useTheme } from "next-themes";
import { useShallow } from "zustand/react/shallow";
import useLocalStorage from "@/lib/hooks/useLocalStorage";
import { cn } from "@/lib/utils";
import { auth } from "@/lib/utils/firebase/config";
Expand All @@ -17,6 +18,7 @@ import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import LogoutButton from "@/components/auth/LogoutButton";
import Logo from "@/components/global/Logo";
import { useUserDataStore } from "@/components/providers/user-data-store-provider";
import { Button } from "../ui/button";
import { useToast } from "../ui/use-toast";
import EmailVerificationBanner from "./EmailVerificationBanner";
Expand Down Expand Up @@ -44,15 +46,16 @@ type AuthenticatedHeaderProps = {
const AuthenticatedHeader: React.FC<AuthenticatedHeaderProps> = ({
hideLogout,
}) => {
const [emailVerified, setEmailVerified] = React.useState(
auth.currentUser?.emailVerified
const isAuthenticated = useUserDataStore((state) => state.isAuthenticated);
const isEmailVerified = useUserDataStore((state) => state.isEmailVerified);
const verifyAndSetEmailVerified = useUserDataStore(
(state) => state.verifyAndSetEmailVerified
);

const [bannerDismissed, setBannerDismissed] = useState(false);

useEffect(() => {
setEmailVerified(auth.currentUser?.emailVerified);
}, [auth.currentUser]);
verifyAndSetEmailVerified(isEmailVerified);
}, [verifyAndSetEmailVerified]);

const { toast: displayToast } = useToast();

Expand All @@ -73,7 +76,7 @@ const AuthenticatedHeader: React.FC<AuthenticatedHeaderProps> = ({
});
return;
}
if (auth.currentUser && !auth.currentUser?.emailVerified) {
if (auth.currentUser && !isEmailVerified) {
await sendEmailVerification(auth.currentUser);
setLastAuthRequestSent(Date.now().toString());
displayToast({
Expand All @@ -93,7 +96,7 @@ const AuthenticatedHeader: React.FC<AuthenticatedHeaderProps> = ({

return (
<>
{!emailVerified && !bannerDismissed && (
{isAuthenticated && !isEmailVerified && !bannerDismissed && (
<EmailVerificationBanner
onDismiss={() => {
setBannerDismissed(true);
Expand All @@ -117,7 +120,7 @@ const AuthenticatedHeader: React.FC<AuthenticatedHeaderProps> = ({
</div>
<DropdownMenuSeparator className="my-4" />
<ThemeSwitch />
{emailVerified === false && (
{isAuthenticated && !isEmailVerified && (
<>
<DropdownMenuSeparator className="my-4" />
<Button
Expand All @@ -130,7 +133,7 @@ const AuthenticatedHeader: React.FC<AuthenticatedHeaderProps> = ({
</>
)}
<DropdownMenuSeparator className="my-4" />
<LogoutButton buttonClass="w-full" />
{isAuthenticated && <LogoutButton buttonClass="w-full" />}
</DropdownMenuContent>
</DropdownMenu>
) : (
Expand Down
59 changes: 59 additions & 0 deletions src/components/providers/user-data-store-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"use client";

import {
type ReactNode,
createContext,
useContext,
useEffect,
useRef,
} from "react";
import { type StoreApi, useStore } from "zustand";
import {
type UserDataStore,
createUserDataStore,
defaultUserStoreData,
initUserDataStore,
} from "@/lib/stores/user/userStore";

export const UserDataStoreContext =
createContext<StoreApi<UserDataStore> | null>(null);

export interface UserDataStoreProviderProps {
children: ReactNode;
}

export const UserDataStoreProvider = ({
children,
}: UserDataStoreProviderProps) => {
const storeRef = useRef<StoreApi<UserDataStore>>();

if (!storeRef.current) {
storeRef.current = createUserDataStore(defaultUserStoreData);
}

useEffect(() => {
initUserDataStore().then((data) => {
storeRef.current?.setState(data);
});
}, []);

return (
<UserDataStoreContext.Provider value={storeRef.current}>
{children}
</UserDataStoreContext.Provider>
);
};

export const useUserDataStore = <T,>(
selector: (store: UserDataStore) => T
): T => {
const counterStoreContext = useContext(UserDataStoreContext);

if (!counterStoreContext) {
throw new Error(
`useUserDataStore must be use within UserDataStoreProvider`
);
}

return useStore(counterStoreContext, selector);
};
80 changes: 80 additions & 0 deletions src/lib/stores/user/userStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { create } from "zustand";
import { auth } from "@/lib/utils/firebase/config";

type UserBasicData = {
name?: string;
email: string;
emailVerified?: boolean;
uid: string;
};

type UserDataStoreState = {
userData?: UserBasicData;
isAuthenticated: boolean;
isEmailVerified: boolean;
};

type UserDataStoreActions = {
setIsEmailVerified: (value: boolean) => void;
verifyAndSetEmailVerified: (emailAlreadyVerified?: boolean) => Promise<void>;
setIsAuthenticated: (value: boolean) => void;
setUserData: (data: UserBasicData) => void;
fetchAndSetUserData: () => Promise<void>;
};

type UserDataStore = UserDataStoreState & UserDataStoreActions;

const defaultUserStoreData: UserDataStoreState = {
isAuthenticated: false,
isEmailVerified: false,
userData: undefined,
};

const initUserDataStore = async (): Promise<UserDataStoreState> => {
await auth.authStateReady();
const user = auth.currentUser;
if (user) {
return {
userData: {
email: user.email || "",
emailVerified: user.emailVerified,
uid: user.uid,
},
isEmailVerified: user.emailVerified,
isAuthenticated: true,
};
} else {
return defaultUserStoreData;
}
};

const createUserDataStore = (
initState: UserDataStoreState = defaultUserStoreData
) => {
return create<UserDataStore>((set) => ({
...initState,
setIsAuthenticated: (value: boolean) => set({ isAuthenticated: value }),
setIsEmailVerified: (value: boolean) => set({ isEmailVerified: value }),
setUserData: (data: UserBasicData) => set({ userData: data }),
fetchAndSetUserData: async () => {
const data = await initUserDataStore();
set(data);
},
verifyAndSetEmailVerified: async (emailAlreadyVerified?: boolean) => {
if (emailAlreadyVerified) {
return;
}
await auth.authStateReady();
const user = auth.currentUser;
if (user) {
set({ isEmailVerified: user.emailVerified });
} else {
set({ isEmailVerified: false });
}
},
resetData: () => set({ ...defaultUserStoreData }),
}));
};

export { initUserDataStore, createUserDataStore, defaultUserStoreData };
export type { UserDataStore };

0 comments on commit c5a8438

Please sign in to comment.