diff --git a/.env b/.env
index b8e0f03..b5ec710 100644
--- a/.env
+++ b/.env
@@ -1,5 +1,5 @@
VITE_GOOGLE_CLIENT_ID=https://272098283932-bft2gvlgjn8edopg0lnqjq1i9ekdmipt.apps.googleusercontent.com/
-VITE_DEFUALT_AUTH_RETURN_URL=/signup
+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
VITE_IDENTITY_CLIENT_ID=harmony_identity
diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx
index 0550805..6bb7ab2 100644
--- a/src/components/ProtectedRoute.tsx
+++ b/src/components/ProtectedRoute.tsx
@@ -1,9 +1,11 @@
-import { getAccessToken } from '@/features/authorization/api/identityAPI';
+import { useAuth } from '@/hooks/useAuth';
import { type PropsWithChildren } from 'react';
import { Navigate } from 'react-router-dom';
export const ProtectedRoute = ({ children }: PropsWithChildren) => {
- if (!getAccessToken()) {
+ const auth = useAuth();
+
+ if (!auth.accessToken) {
// If no token, redirect to login page
return ;
}
diff --git a/src/contexts/AuthContext.ts b/src/contexts/AuthContext.ts
new file mode 100644
index 0000000..03d9528
--- /dev/null
+++ b/src/contexts/AuthContext.ts
@@ -0,0 +1,16 @@
+import { createContext } from 'react';
+
+type AuthContextType = {
+ accessToken: string | null;
+ getToken: () => string | null;
+ login: (tokens: {
+ access_token: string;
+ refresh_token: string;
+ expires_in: number;
+ }) => void;
+ logout: () => void;
+};
+
+export const AuthContext = createContext(
+ undefined,
+);
diff --git a/src/features/authorization/api/identityAPI.ts b/src/features/authorization/api/identityAPI.ts
index f06b7d8..0d20e01 100644
--- a/src/features/authorization/api/identityAPI.ts
+++ b/src/features/authorization/api/identityAPI.ts
@@ -18,7 +18,7 @@ const ACCESS_TOKEN_KEY: 'access_token' = 'access_token' as const;
const REFRESH_TOKEN_KEY: 'refresh_token' = 'refresh_token' as const;
const EXPIRES_IN_KEY: 'expires_in' = 'expires_in' as const;
-export const generateToken = async (request: GenerateToken) => {
+export const generateTokenWithPassword = (request: GenerateToken) => {
const body = new URLSearchParams();
body.set('grant_type', 'password');
body.set('client_id', import.meta.env.VITE_IDENTITY_CLIENT_ID);
@@ -26,7 +26,7 @@ export const generateToken = async (request: GenerateToken) => {
body.set('username', request.username);
body.set('password', request.password);
- const result = await apiClient.post(
+ return apiClient.post(
import.meta.env.VITE_IDENTITY_URL,
body.toString(),
{
@@ -35,75 +35,4 @@ export const generateToken = async (request: GenerateToken) => {
},
},
);
-
- sessionStorage.setItem(ACCESS_TOKEN_KEY, result.data.access_token);
- sessionStorage.setItem(REFRESH_TOKEN_KEY, result.data.refresh_token);
- sessionStorage.setItem(
- EXPIRES_IN_KEY,
- String(Date.now() + result.data.expires_in * 1000),
- );
};
-
-export const getAccessToken = () => {
- const sessionToken = sessionStorage.getItem(ACCESS_TOKEN_KEY);
-
- if (sessionToken) return null;
-
- const expiresAt = Number(sessionStorage.getItem(EXPIRES_IN_KEY));
- const now = Date.now();
-
- if (now < expiresAt) {
- return sessionToken;
- } else {
- return refreshAccessToken();
- }
-};
-
-const refreshAccessToken = async () => {
- const refreshToken = sessionStorage.getItem(REFRESH_TOKEN_KEY);
- if (!refreshToken) return null;
-
- const result = await axios.post(
- import.meta.env.VITE_IDENTITY_URL,
- new URLSearchParams({
- grant_type: 'refresh_token',
- refresh_token: refreshToken,
- client_id: 'harmony_identity', // from your token payload
- }),
- {
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- },
- );
-
- if (result.data.access_token) {
- sessionStorage.setItem(ACCESS_TOKEN_KEY, result.data.access_token);
- sessionStorage.setItem(
- EXPIRES_IN_KEY,
- String(Date.now() + result.data.expires_in * 1000),
- );
- return result.data.access_token;
- }
-
- return null;
-};
-
-// export const generateToken = (request: any) => {
-// const body = new URLSearchParams();
-// body.set('grant_type', request.password ? 'password' : 'otp');
-// body.set('client_id', import.meta.env.VITE_IDENTITY_CLIENT_ID);
-// body.set('scope', import.meta.env.VITE_IDENTITY_SCOPE);
-// if (request.phoneNumber) body.set('phonenumber', request.phoneNumber);
-// if (request.email) body.set('email', request.email);
-// if (request.password) body.set('password', request.password);
-// if (request.otp) body.set('otp', request.otp);
-
-// return axios
-// .post(import.meta.env.VITE_IDENTITY_URL, body.toString(), {
-// headers: {
-// 'Content-Type': 'application/x-www-form-urlencoded',
-// },
-// })
-// .then((result) => console.log(result));
-// };
diff --git a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx
index e0ff695..ad240b8 100644
--- a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx
+++ b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx
@@ -62,7 +62,7 @@ export const AuthenticationSteps = (): JSX.Element => {
};
const handlePhoneNumberVerified = () => {
- redirectToReturnUrl();
+ navigate('/signup');
};
const redirectToReturnUrl = () => {
diff --git a/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx
index 59de5b2..26d262c 100644
--- a/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx
+++ b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx
@@ -21,7 +21,8 @@ import {
import type { PasswordLoginRequest } from '../../types/userTypes';
import { Icon, useToast } from '@rkheftan/harmony-ui';
import { useApi } from '@/hooks/useApi';
-import { generateToken } from '../../api/identityAPI';
+import { useAuth } from '@/hooks/useAuth';
+import { generateTokenWithPassword } from '../../api/identityAPI';
export interface EnterPasswordFormProps {
onEditValue: () => void;
@@ -59,6 +60,7 @@ export const EnterPasswordForm = ({
loading: loginWithPassLoading,
execute: loginWithPassCall,
} = useApi(loginWithPassword);
+ const auth = useAuth();
const handleBlur = () => {
setInputTouched(true);
@@ -80,12 +82,13 @@ export const EnterPasswordForm = ({
if (!loginWithPassResult) return;
if (loginWithPassResult.success) {
- console.log('logged in');
-
- await generateToken({
+ const tokenRes = await generateTokenWithPassword({
username: apiRequest.email ?? (apiRequest.phoneNumber as string),
password: apiRequest.password,
});
+ auth.login({
+ ...tokenRes.data,
+ });
onLoggedIn(loginWithPassResult.userId);
diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts
new file mode 100644
index 0000000..fb4705a
--- /dev/null
+++ b/src/hooks/useAuth.ts
@@ -0,0 +1,8 @@
+import { AuthContext } from '@/contexts/AuthContext';
+import { useContext } from 'react';
+
+export const useAuth = () => {
+ const ctx = useContext(AuthContext);
+ if (!ctx) throw new Error('useAuth must be used within an AuthProvider');
+ return ctx;
+};
diff --git a/src/lib/apiClient.ts b/src/lib/apiClient.ts
index 346a0b5..ee63a48 100644
--- a/src/lib/apiClient.ts
+++ b/src/lib/apiClient.ts
@@ -1,4 +1,4 @@
-import { getAccessToken } from '@/features/authorization/api/identityAPI';
+import { ACCESS_TOKEN_KEY } from '@/providers/AuthProvider';
import axios from 'axios';
const apiClient = axios.create({
@@ -18,8 +18,8 @@ const apiClient = axios.create({
// --- Request Interceptor ---
// This runs BEFORE each request is sent
apiClient.interceptors.request.use(
- async (config) => {
- const token = await getAccessToken();
+ (config) => {
+ const token = sessionStorage.getItem(ACCESS_TOKEN_KEY);
if (token) {
// Add the authorization token to the headers
config.headers.Authorization = `Bearer ${token}`;
diff --git a/src/providers/AppProvider.tsx b/src/providers/AppProvider.tsx
index f68f2ae..66efe98 100644
--- a/src/providers/AppProvider.tsx
+++ b/src/providers/AppProvider.tsx
@@ -4,6 +4,7 @@ import i18n from '@/config/i18n';
import { CustomThemeProvider } from './CustomThemeProvider';
import { RtlProvider } from './RtlProvider';
import { ToastProvider } from '@rkheftan/harmony-ui';
+import { AuthProvider } from './AuthProvider';
export const AppProviders: React.FC<{ children: React.ReactNode }> = ({
children,
@@ -12,7 +13,9 @@ export const AppProviders: React.FC<{ children: React.ReactNode }> = ({
- {children}
+
+ {children}
+
diff --git a/src/providers/AuthProvider.tsx b/src/providers/AuthProvider.tsx
new file mode 100644
index 0000000..7b2ae27
--- /dev/null
+++ b/src/providers/AuthProvider.tsx
@@ -0,0 +1,119 @@
+// useAuth.tsx
+import { AuthContext } from '@/contexts/AuthContext';
+import type { GenerateTokenResponse } from '@/features/authorization/api/identityAPI';
+import axios from 'axios';
+import { useEffect, useState, type ReactNode } from 'react';
+
+export const ACCESS_TOKEN_KEY: 'access_token' = 'access_token' as const;
+export const REFRESH_TOKEN_KEY: 'refresh_token' = 'refresh_token' as const;
+export const EXPIRES_IN_KEY: 'expires_in' = 'expires_in' as const;
+
+let inMemoryToken: string | null = null;
+let expiresAt = 0;
+
+export const AuthProvider = ({ children }: { children: ReactNode }) => {
+ 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);
+
+ if (token && Date.now() < exp) {
+ inMemoryToken = token;
+ expiresAt = exp;
+ setAccessToken(token);
+ }
+ }, []);
+
+ // Background refresh
+ useEffect(() => {
+ const handleRefreshTokenIfNeeded = async () => {
+ if (!inMemoryToken) return;
+ if (Date.now() < expiresAt - 60_000) return; // still valid (buffer)
+
+ await refreshAccessToken();
+ setAccessToken(inMemoryToken);
+ };
+
+ handleRefreshTokenIfNeeded();
+
+ const interval = setInterval(async () => {
+ await handleRefreshTokenIfNeeded();
+ }, 30_000);
+
+ return () => clearInterval(interval);
+ }, []);
+
+ function login(tokens: {
+ access_token: string;
+ refresh_token: string;
+ expires_in: number;
+ }) {
+ inMemoryToken = tokens.access_token;
+ expiresAt = Date.now() + tokens.expires_in * 1000;
+
+ sessionStorage.setItem(ACCESS_TOKEN_KEY, tokens.access_token);
+ sessionStorage.setItem(REFRESH_TOKEN_KEY, tokens.refresh_token);
+ sessionStorage.setItem(EXPIRES_IN_KEY, String(expiresAt));
+
+ setAccessToken(tokens.access_token);
+ }
+
+ function logout() {
+ inMemoryToken = null;
+ expiresAt = 0;
+ sessionStorage.clear();
+ setAccessToken(null);
+ }
+
+ function getToken() {
+ return inMemoryToken;
+ }
+
+ async function refreshAccessToken() {
+ const refreshToken = sessionStorage.getItem(REFRESH_TOKEN_KEY);
+ if (!refreshToken) {
+ logout();
+ return;
+ }
+
+ try {
+ const result = await axios.post(
+ import.meta.env.VITE_IDENTITY_URL,
+ new URLSearchParams({
+ grant_type: 'refresh_token',
+ refresh_token: refreshToken,
+ client_id: import.meta.env.VITE_IDENTITY_CLIENT_ID, // from your token payload
+ }),
+ {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ },
+ );
+
+ if (result.data.access_token) {
+ inMemoryToken = result.data.access_token;
+ expiresAt = Date.now() + result.data.expires_in * 1000;
+
+ sessionStorage.setItem(ACCESS_TOKEN_KEY, inMemoryToken as string);
+ sessionStorage.setItem(EXPIRES_IN_KEY, String(expiresAt));
+
+ setAccessToken(inMemoryToken);
+ } else {
+ logout();
+ }
+ } catch {
+ logout();
+ }
+ }
+
+ return (
+
+ {children}
+
+ );
+};