diff --git a/.env b/.env
index b5ec710..cfdb88a 100644
--- a/.env
+++ b/.env
@@ -1,4 +1,4 @@
-VITE_GOOGLE_CLIENT_ID=https://272098283932-bft2gvlgjn8edopg0lnqjq1i9ekdmipt.apps.googleusercontent.com/
+VITE_GOOGLE_CLIENT_ID=https://272098283932-bft2gvlgjn8edopg0lnqjq1i9ekdmipt.apps.googleusercontent.com
VITE_DEFUALT_AUTH_RETURN_URL=/setting/profile
VITE_API_URL=https://accounts.business-harmony.com/api/
VITE_IDENTITY_URL=https://accounts.business-harmony.com/connect/token
diff --git a/package-lock.json b/package-lock.json
index 20a2b64..1f0ab8a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,6 +24,7 @@
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"iconsax-react": "^0.0.8",
+ "jwt-decode": "^4.0.0",
"libphonenumber-js": "^1.12.10",
"react": "^19.1.0",
"react-country-flag": "^3.1.0",
@@ -4344,6 +4345,15 @@
"safe-buffer": "~5.1.0"
}
},
+ "node_modules/jwt-decode": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
+ "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
diff --git a/package.json b/package.json
index 4215562..c803456 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"iconsax-react": "^0.0.8",
+ "jwt-decode": "^4.0.0",
"libphonenumber-js": "^1.12.10",
"react": "^19.1.0",
"react-country-flag": "^3.1.0",
diff --git a/src/App.tsx b/src/App.tsx
index ec7f7b6..12661e1 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -3,15 +3,23 @@ import './App.css';
import { LanguageManager } from '@/components/LanguageManager';
import { RouterProvider } from 'react-router-dom';
import { router } from '@/routes';
+import { useAuth } from './hooks/useAuth';
+import { Loading } from './components/Loading';
function App() {
- return (
- <>
-
-
-
- >
- );
+ const { authFinished } = useAuth();
+
+ if (authFinished) {
+ return (
+ <>
+
+
+
+ >
+ );
+ }
+
+ return ;
}
export default App;
diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx
index 39f4eb9..9121b43 100644
--- a/src/components/Layout/Header.tsx
+++ b/src/components/Layout/Header.tsx
@@ -1,10 +1,10 @@
import { Box, IconButton, Typography } from '@mui/material';
import { Icon } from '@rkheftan/harmony-ui';
import { More } from 'iconsax-react';
-import type { User } from './type';
+import type { UserInfo } from '@/contexts/AuthContext';
interface HeaderProps {
- user: User;
+ user: UserInfo;
}
export const Header: React.FC = ({ user }) => {
@@ -19,11 +19,9 @@ export const Header: React.FC = ({ user }) => {
}}
>
-
- {user.firstName + ' ' + user.lastName}
-
+ {user.fullName}
- {user.phoneNumber}
+ {user.phoneNumber ?? user.email}
diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx
index 2652dfe..d2ac9bb 100644
--- a/src/components/Layout/Layout.tsx
+++ b/src/components/Layout/Layout.tsx
@@ -6,7 +6,7 @@ import { Box, useMediaQuery, useTheme } from '@mui/material';
import { Header } from './Header';
import { useState } from 'react';
import { Toolbar } from './Toolbar';
-import type { User } from './type';
+import { useAuth } from '@/hooks/useAuth';
export const Layout = () => {
const navItemConfigs = buildNavItems(appRoutes);
@@ -14,11 +14,8 @@ export const Layout = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [sideNavOpen, setSideNavOpen] = useState(false);
- const [user] = useState({
- firstName: 'محمد حسین',
- lastName: 'برزه گر',
- phoneNumber: '09123456789',
- });
+
+ const { userInfo } = useAuth();
return (
{
isMobile={isMobile}
sideNavOpen={sideNavOpen}
setSideNavOpen={setSideNavOpen}
- user={user}
+ user={userInfo}
/>
@@ -54,8 +51,8 @@ export const Layout = () => {
setSideNavOpen(false)}
- header={isMobile ? undefined : }
- footer={isMobile ? : undefined}
+ header={isMobile ? undefined : }
+ footer={isMobile ? : undefined}
navConfig={navItemConfigs}
activePath={location.pathname + location.hash}
selectedVariant="textOnly"
diff --git a/src/components/Layout/Toolbar.tsx b/src/components/Layout/Toolbar.tsx
index 46b482e..1b6ddea 100644
--- a/src/components/Layout/Toolbar.tsx
+++ b/src/components/Layout/Toolbar.tsx
@@ -8,13 +8,13 @@ import {
import { Icon } from '@rkheftan/harmony-ui';
import { HambergerMenu, Menu } from 'iconsax-react';
import type { Dispatch, SetStateAction } from 'react';
-import type { User } from './type';
+import type { UserInfo } from '@/contexts/AuthContext';
interface ToolbarProps {
sideNavOpen: boolean;
setSideNavOpen: Dispatch>;
isMobile: boolean;
- user: User;
+ user: UserInfo;
}
export const Toolbar: React.FC = ({
@@ -61,7 +61,7 @@ export const Toolbar: React.FC = ({
{isMobile && (
{user.firstName.charAt(0) + ' ' + user.lastName.charAt(0)}
diff --git a/src/components/Layout/type.ts b/src/components/Layout/type.ts
index ab31407..e69de29 100644
--- a/src/components/Layout/type.ts
+++ b/src/components/Layout/type.ts
@@ -1,8 +0,0 @@
-// TODO: this type file is temporary and should replace it with the actual use type and value when api is ready
-
-export interface User {
- firstName: string;
- lastName: string;
- phoneNumber: string;
- profileUrl?: string;
-}
diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx
new file mode 100644
index 0000000..1572b6a
--- /dev/null
+++ b/src/components/Loading.tsx
@@ -0,0 +1,19 @@
+import { CircularProgress, Stack } from '@mui/material';
+import React from 'react';
+
+export const Loading = () => {
+ return (
+ t.palette.background.default,
+ position: 'fixed',
+ inset: '0',
+ zIndex: (t) => t.zIndex.tooltip + 1,
+ }}
+ >
+
+
+ );
+};
diff --git a/src/contexts/AuthContext.ts b/src/contexts/AuthContext.ts
index 03d9528..c240809 100644
--- a/src/contexts/AuthContext.ts
+++ b/src/contexts/AuthContext.ts
@@ -1,5 +1,16 @@
+import type { GUID } from '@/types/commonTypes';
import { createContext } from 'react';
+export interface UserInfo {
+ picture: string;
+ firstName: string;
+ lastName: string;
+ fullName: string;
+ email: string;
+ phoneNumber: string;
+ userID: GUID;
+}
+
type AuthContextType = {
accessToken: string | null;
getToken: () => string | null;
@@ -9,6 +20,8 @@ type AuthContextType = {
expires_in: number;
}) => void;
logout: () => void;
+ authFinished: boolean;
+ userInfo: UserInfo;
};
export const AuthContext = createContext(
diff --git a/src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx b/src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx
index c343a35..d517362 100644
--- a/src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx
+++ b/src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx
@@ -37,7 +37,7 @@ export const GoogleAuthentication = ({
client_id: import.meta.env.VITE_GOOGLE_CLIENT_ID,
scope: 'openid email profile',
ux_mode: 'popup',
- response_type: 'id_token',
+ response_type: 'code',
callback: async (resp: GoogleCodeClientResponse) => {
const res = await loginWithGoogleCall({
idToken: resp.id_token,
diff --git a/src/providers/AuthProvider.tsx b/src/providers/AuthProvider.tsx
index 7b2ae27..b18aecb 100644
--- a/src/providers/AuthProvider.tsx
+++ b/src/providers/AuthProvider.tsx
@@ -1,8 +1,31 @@
// useAuth.tsx
-import { AuthContext } from '@/contexts/AuthContext';
+import { AuthContext, type UserInfo } from '@/contexts/AuthContext';
import type { GenerateTokenResponse } from '@/features/authorization/api/identityAPI';
import axios from 'axios';
import { useEffect, useState, type ReactNode } from 'react';
+import { jwtDecode } from 'jwt-decode';
+import type { GUID } from '@/types/commonTypes';
+
+export interface AccessTokenJwtPayload {
+ iss: string;
+ nbf: number;
+ iat: number;
+ exp: number;
+ scope: string[];
+ amr: string[];
+ client_id: string;
+ sub: GUID;
+ auth_time: number;
+ idp: string;
+ picture: string;
+ given_name: string;
+ family_name: string;
+ email: string;
+ name: string;
+ phone_number: string;
+ session: GUID;
+ jti: string;
+}
export const ACCESS_TOKEN_KEY: 'access_token' = 'access_token' as const;
export const REFRESH_TOKEN_KEY: 'refresh_token' = 'refresh_token' as const;
@@ -12,20 +35,31 @@ let inMemoryToken: string | null = null;
let expiresAt = 0;
export const AuthProvider = ({ children }: { children: ReactNode }) => {
+ const [userInfo, setUserInfo] = useState(null);
+ const [authFinished, setAuthFinished] = useState(false);
const [accessToken, setAccessToken] = useState(
sessionStorage.getItem(ACCESS_TOKEN_KEY),
);
// Initialize from sessionStorage (page reload)
useEffect(() => {
- const token = sessionStorage.getItem(ACCESS_TOKEN_KEY);
- const exp = Number(sessionStorage.getItem(EXPIRES_IN_KEY) || 0);
+ const handleAndSetToken = async () => {
+ const token = sessionStorage.getItem(ACCESS_TOKEN_KEY);
- if (token && Date.now() < exp) {
- inMemoryToken = token;
- expiresAt = exp;
- setAccessToken(token);
- }
+ if (token) {
+ setUserInfoFromToken(token);
+ await refreshAccessToken();
+ inMemoryToken = token;
+ expiresAt = Number(sessionStorage.getItem(EXPIRES_IN_KEY) || 0);
+ setAccessToken(token);
+
+ setAuthFinished(true);
+ }
+
+ setAuthFinished(true);
+ };
+
+ handleAndSetToken();
}, []);
// Background refresh
@@ -38,8 +72,6 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
setAccessToken(inMemoryToken);
};
- handleRefreshTokenIfNeeded();
-
const interval = setInterval(async () => {
await handleRefreshTokenIfNeeded();
}, 30_000);
@@ -100,6 +132,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
expiresAt = Date.now() + result.data.expires_in * 1000;
sessionStorage.setItem(ACCESS_TOKEN_KEY, inMemoryToken as string);
+ sessionStorage.setItem(REFRESH_TOKEN_KEY, result.data.refresh_token);
sessionStorage.setItem(EXPIRES_IN_KEY, String(expiresAt));
setAccessToken(inMemoryToken);
@@ -111,8 +144,26 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
}
}
+ function setUserInfoFromToken(token: string) {
+ const accessTokenPayload = jwtDecode(token);
+
+ const userInfo: UserInfo = {
+ picture: accessTokenPayload.picture,
+ firstName: accessTokenPayload.given_name,
+ lastName: accessTokenPayload.family_name,
+ fullName: accessTokenPayload.name,
+ email: accessTokenPayload.email,
+ phoneNumber: accessTokenPayload.phone_number,
+ userID: accessTokenPayload.sub,
+ };
+
+ setUserInfo(userInfo);
+ }
+
return (
-
+
{children}
);
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 53b7482..9980e7d 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -2,6 +2,7 @@ import { Suspense, type ReactNode } from 'react';
import { createBrowserRouter, type RouteObject } from 'react-router-dom';
import { appRoutes, type RouteConfig } from './config';
import { ProtectedRoute } from '@/components/ProtectedRoute';
+import { Loading } from '@/components/Loading';
/**
* A recursive function to map our custom route config to the format
@@ -11,7 +12,7 @@ function mapRoutes(routes: RouteConfig[]): RouteObject[] {
return routes.map((route) => {
// Start with the base element, wrapped in Suspense for lazy loading
let element: ReactNode = (
- Loading...}>{route.element}
+ }>{route.element}
);
// Conditionally wrap the element in the specified layout