This is the original apiClientServices.ts

```
// src/services/apiClientServices.ts
import axios, { AxiosRequestConfig } from 'axios';
import { API_BASE_URL } from '../constants/apiConstants';
import { emitEvent } from '../utils/eventBusUtils';
import { csrfTokenManager } from './csrfServices';
import { getAccessTokenNative, clearTokensNative } from './nativeTokenStorageServices';// Native app - Secure Authorization token storage
import { tryRefreshToken } from './authServices';// Refresh Jwt secure token storage
import logger from '../utils/loggerUtils';


/**
* API Return Structure for All Responses:
*
* 'success' => bool
* - Required boolean indicating the overall success (true) or failure (false) of the request.
*
* 'message' => string|null
* - Optional human-readable summary message intended for general user feedback.
*   For example: "Signup completed successfully." or "Validation failed. Please review errors."
*
* 'errors' => array|null
* - Optional detailed field-level errors provided as an associative array keyed by attribute name,
*   e.g. ['email' => ['Email already registered'], 'password' => ['Password too weak']]
* - Should be included typically for validation failures or form errors.
* - Should NOT aggregate all field errors into this or the 'message' string.
*
* 'data' => array|null
* - Optional container for domain-specific returned data relevant to the successful request,
*   such as user IDs, signup phases, tokens, or other payload.
*   Example: ['userId' => 123, 'signup_phase' => 2]
*
* Guidelines:
* - Always use the 'success' boolean to convey request outcome; do NOT rely on other status indicators.
* - Always include a clear and concise 'message' to aid frontend user messaging.
* - Only include 'errors' when there are validation or field-specific errors to display inline.
* - Group all additional return values inside the 'data' object to keep the response consistent.
* - Use appropriate HTTP status codes (e.g. 422 for validation errors, 200 for success) together
*   with this body structure for clarity and RESTfulness.
*
* Example error response:
* 	[
*		'success' => false,
*		'message' => 'Validation failed. Please review the errors.',
* 		'errors' => $model->getErrors(),
* 		// 'data' omitted
* 	]
*
* Example success response:
* 	[
* 		'success' => true,
*		'message' => 'Signup completed successfully.',
* 		'errors' => [],// or omitted
* 		'data' => [
*  			'userId' => $user->id,
* 			'signup_phase' => $user->signup_phase,
* 		]
*	 ]
*/
const apiClient = axios.create({
	baseURL: API_BASE_URL,
	withCredentials: true,// sends cookies automatically with cross-origin requests
	headers:
	{
		'Content-Type': 'application/json',
	},
	timeout: 10000,// Aborts the request and throws an error after 10 seconds
});

/**
* Add request interceptor
*
* Axios interceptors allow you to intercept and modify requests or responses globally before 
* they are handled by your individual API calls.
*
* - Request interceptors run before a request is sent, letting you add headers (e.g., auth tokens), 
*   modify request config, or log requests.
* - Response interceptors run after receiving a response, enabling centralized error handling, token 
*   refresh logic, or response transformation.
* 
* Using interceptors helps keep your API client DRY, consistent, and easier to maintain.
*/
apiClient.interceptors.request.use(
	async (config: AxiosRequestConfig) =>
	{
		/**
		* Read token from the non-HttpOnly cookie or from csrf-token endpoint
		* add X-CSRF-Token header to mutating requests (POST, PUT, PATCH, DELETE)
		*/
		const method = config.method ? config.method.toLowerCase() : '';
		if (['post', 'put', 'patch', 'delete'].includes(method))
		{
			const token = csrfTokenManager.getRawToken();
			
			if (token)
			{
				config.headers = config.headers || {};
				config.headers['X-CSRF-Token'] = token;
			}
		}
		
		/**
		* Authorization header is only needed for Native React App. Web browser
		* based apps store JWT token within cookies using HttpOnly.
		*
		* JWT in an HTTP-only Secure Cookie
		* If you store the JWT in an HTTP-only secure cookie and use `withCredentials: true`,
		* the browser automatically sends the cookie with every request.
		*
		* In this case, manually attaching the Authorization header with the JWT 
		* from localStorage is redundant and defeats the purpose of using HTTP-only cookies,
		* since HTTP-only cookies are inaccessible to JavaScript.
		*
		* To maintain security best practices and avoid token duplication, never store
		* tokens in localStorage, indexedDB, Sessions and memory. Storing JWT tokens 
		* exposes teh app to XSS attacks, since malicious scripts could access these
		* storages.
		*/
		const token = await getAccessTokenNative();
		
		if (token && config.headers)
		{
			config.headers['Authorization'] = `Bearer ${token}`;
		}//*/
		
		return config;// Always return config
	},
	(error) => Promise.reject(error)
);//*/

/**
* Add response interceptor to handle errors globally, refresh tokens, etc.
*/
apiClient.interceptors.response.use
(
	(response) => response,// Pass through successful responses unchanged
	
	async (error) =>
	{
		// Handle Axios timeout errors explicitly
		if (error.code === 'ECONNABORTED' && error.message.includes('timeout'))
		{
			logger.warn('apiClientServices.ts', 'Request timed out:', error);
			
			// TODO - emitEvent('alert', { type: 'warning', key: 'alert.timeout' });
			
			return Promise.reject(new Error('Request timed out. Please try again.'));
		}
		
		// Otherwise handle by HTTP status code
		const status = error.response?.status;
		
		/**
		* 401 Outside Switch and case (loop)
		* When a request returns a 401 Unauthorized, you want to:
		* - Attempt to refresh the token (e.g., get a new JWT cookie).
		* - Retry the original request once with the new token.
		* - If the retry also returns 401 (e.g., refresh failed or token invalid), 
		*   do not keep retrying indefinitely — instead redirect to login or show error.
		*
		* TODO - Signup needs to ignore this.
		*/
		const originalRequest = error.config;
		
		if (status === 401 && !originalRequest?._retry && !originalRequest.url.includes('/signup/'))
		{
			originalRequest._retry = true;// Prevent infinite loop
			
			logger.warn('apiClientServices.ts', '401 Unauthorized - attempting token refresh');
			
			try
			{
				// POST - Call for jwt token for user
				const refreshed = await tryRefreshToken();
				
				if (refreshed)
				{
					logger.info('apiClientServices.ts', 'Token refreshed - retrying original request');
					
					// On refresh success, retry original request (cookies sent automatically)
					return apiClient(originalRequest);
				}
				else
				{
					// Refresh failed => redirect to login
					logger.error('apiClientServices.ts', 'Token refresh failed - redirecting to login');
					
					// Native React App - Request to clear token
					await clearTokensNative();
					
					// TODO - emitEvent('alert', { type: 'danger', key: 'alert.unauthorized' });
					
					window.location.href = '/login';// adjust as needed
					
					return Promise.reject(error);
				}
			}
			catch (refreshError)
			{
				logger.error('apiClientServices.ts', 'Error during token refresh:', refreshError);
				
				// Native React App - Request to clear token
				await clearTokensNative();
				
				// TODO - emitEvent('alert', { type: 'danger', key: 'alert.unauthorized' });
				
				window.location.href = '/login';
				
				return Promise.reject(error);
			}
		}//*/
		
		// Other status code specific handling for logging and alerts
		switch (status)
		{
			case 400 :
			{
				// Bad Request — maybe validation failed
				logger.warn('apiClientServices.ts', '400 Bad Request:', error);
				
				// TODO - emitEvent('alert', { type: 'warning', key: 'alert.badRequest' });
				
				break;
			}
			//case 401 : {}
			case 403 :
			{
				// Forbidden — user lacks permissions
				logger.warn('apiClientServices.ts', '403 Forbidden:', error);
				
				// TODO - emitEvent('alert', { type: 'danger', key: 'alert.forbidden' });
				
				// Maybe redirect or show permission error dialog
				
				break;
			}
			case 404 :
			{
				// Not Found — resource missing
				logger.info('apiClientServices.ts', '404 Not Found:', error);
				
				// TODO - emitEvent('alert', { type: 'info', key: 'alert.notFound' });
				
				break;
			}
			case 500 :
			{
				// Server error — inform user, retry options, etc
				logger.error('apiClientServices.ts', '500 Server Error:', error);
				
				// TODO - emitEvent('alert', { type: 'danger', key: 'alert.serverError' });
				
				break;
			}	
			default :
			{
				// Other errors: network issues, etc.
				logger.error('apiClientServices.ts', 'Unhandled error:', error);
				
				// TODO - emitEvent('alert', { type: 'danger', key: 'alert.unknownError' });
			}
		}
	
		// handle 401, token refresh, global error logging here
		return Promise.reject(error);
	}
);

export default apiClient;
```

Suggested changes to improve on it have been made:

```
// src/services/authTokenExpiryServices.ts
import { AsyncCache } from '../utils/asyncCacheUtils';
import { CompositeCacheStorage } from '../utils/compositeCacheStorageUtils';
import { LocalStorageCacheStorage } from '../utils/localStorageCacheStorageUtils';
import { MemoryCacheStorage } from '../utils/memoryCacheStorageUtils';


// You can adjust the storage key and TTL as needed
const STORAGE_KEY = 'authTokenExpiryCache';

/**
* Instantiate storage layers: memory → localStorage (skip IndexedDB for simple value)
*/
const memoryStorage = new MemoryCacheStorage<number>();
const localStorageStorage = new LocalStorageCacheStorage<number>(STORAGE_KEY);

const compositeStorage = new CompositeCacheStorage<number>([
	memoryStorage,
	localStorageStorage,
]);

/**
* TTL for expiry timestamp cache. This can be long because the expiry timestamp itself is authoritative.
* For example: 24 hours, can be higher if you trust backend expiry consistently
*/
const TTL_MS = 24 * 60 * 60 * 1000;

export const accessTokenExpiryCache = new AsyncCache<number>(TTL_MS, compositeStorage);

/**
* Store the expiresAt timestamp in cache
*/
export async function storeAccessTokenExpiry(expiresAtMs: number): Promise<void>
{
	// Using keyless AsyncCache means one value per instance, so empty key is fine
	await accessTokenExpiryCache.set(expiresAtMs);
}

/**
* Get the expiresAt timestamp from cache
*/
export async function getAccessTokenExpiry(): Promise<number | null>
{
	const expiry = await accessTokenExpiryCache.get(null as any);// key is ignored internally
	return expiry ?? null;
}

/**
* Clear cached expiresAt timestamp
*/
export async function clearAccessTokenExpiry(): Promise<void>
{
	await accessTokenExpiryCache.remove(null as any);
}
```

to handle storing 'expireAt' that is sent from the backend:

```
'expiresAt' => $expiresAt * 1000,// milliseconds for JS Date
```

than by storing the expireAt check whether the access token is expired or about to expire within xx minutes.
and call the refresh-jwt-token api, updating the token.

something like this:

```
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { emitEvent } from '../utils/eventBusUtils';
import { csrfTokenManager } from './csrfServices';

// token storage service adapted to store expiresAt timestamp
import {
  getAccessTokenFromStorage,
  storeAccessToken,
  storeAccessTokenExpiry,
  getAccessTokenExpiry,
  refreshToken,
  clearTokens,
} from './tokenStorageServices'; // assume you'd add expiresAt support there

import logger from '../utils/loggerUtils';

const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:5000/api';

const apiClient = axios.create({
  baseURL: API_BASE_URL,
  withCredentials: true,
  headers: { 'Content-Type': 'application/json' },
  timeout: 10000,
});

/**
 * Checks whether the access token is expired or about to expire within 2 minutes.
 * Uses the stored expiresAt timestamp, no JWT decoding required.
 */
async function isAccessTokenExpired(): Promise<boolean> {
  const expiryTimestamp = await getAccessTokenExpiry(); // timestamp in ms

  if (!expiryTimestamp) {
    return true;
  }

  // Current time + 2 minutes buffer must be less than expiry
  const nowWithBuffer = Date.now() + 2 * 60 * 1000;

  return nowWithBuffer >= expiryTimestamp;
}

/**
 * Wraps API call: ensures valid access token by refreshing as needed.
 */
export async function callApiWithAuth<T>(
  apiCall: (token: string) => Promise<AxiosResponse<T>>
): Promise<T> {
  try {
    let token = await getAccessTokenFromStorage();

    if (await isAccessTokenExpired()) {
      // Refresh token call expected to return:
      // { success: boolean; data: { accessToken: string; expiresAt: number } }
      const refreshResponse = await refreshToken();

      if (refreshResponse.success && refreshResponse.data.accessToken && refreshResponse.data.expiresAt) {
        token = refreshResponse.data.accessToken;

        // Store token and expiry timestamp in storage
        await storeAccessToken(token);
        await storeAccessTokenExpiry(refreshResponse.data.expiresAt);
      } else {
        await clearTokens();
        emitEvent('alert', { type: 'danger', key: 'alert.unauthorized' });
        throw new Error('Session expired, please login again');
      }
    }

    const response = await apiCall(token);
    return response.data;
  } catch (error) {
    logger.error('apiClientServices.ts', 'API call error:', error);
    throw error;
  }
}

/**
 * Axios request interceptor: adds CSRF token on mutating requests.
 */
apiClient.interceptors.request.use(async (config: AxiosRequestConfig) => {
  const method = (config.method || '').toLowerCase();
  if (['post', 'put', 'patch', 'delete'].includes(method)) {
    const csrfToken = csrfTokenManager.getRawToken();
    if (csrfToken) {
      config.headers = config.headers || {};
      config.headers['X-CSRF-Token'] = csrfToken;
    }
  }
  return config;
});

/**
 * Axios response interceptor: handles global errors and emits alerts.
 * Do NOT handle token refresh here.
 */
apiClient.interceptors.response.use(
  response => response,
  error => {
    if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
      logger.warn('apiClientServices.ts', 'Request timed out:', error);
      emitEvent('alert', { type: 'warning', key: 'alert.timeout' });
      return Promise.reject(new Error('Request timed out. Please try again.'));
    }

    const status = error.response?.status;

    switch (status) {
      case 400:
        emitEvent('alert', { type: 'warning', key: 'alert.badRequest' });
        break;
      case 403:
        emitEvent('alert', { type: 'danger', key: 'alert.forbidden' });
        break;
      case 404:
        emitEvent('alert', { type: 'info', key: 'alert.notFound' });
        break;
      case 500:
        emitEvent('alert', { type: 'danger', key: 'alert.serverError' });
        break;
      default:
        emitEvent('alert', { type: 'danger', key: 'alert.unknownError' });
    }

    return Promise.reject(error);
  }
);

export default apiClient;
```

Instead of:

```
// token storage service adapted to store expiresAt timestamp
import {
  getAccessTokenFromStorage,
  storeAccessToken,
  storeAccessTokenExpiry,
  getAccessTokenExpiry,
  refreshToken,
  clearTokens,
} from './tokenStorageServices'; // assume you'd add expiresAt support there
```

already have nativeTokenStorageServices.ts that is similar to tokenStorageServices. the nativeTokenStorageServices.ts
has the following:

```
// src/services/nativeTokenStorageServices.ts
/**
* Expo Secure Store
*
* `expo-secure-store` is a secure, encrypted key-value storage library for React Native apps 
* (particularly those built with Expo). It enables safe storage of sensitive data like JWT 
* access and refresh tokens by using the device's secure storage mechanisms:
*   - **iOS Keychain** on iOS devices
*   - **Android Keystore** on Android devices
*
* This significantly reduces exposure to attacks compared to plain AsyncStorage or localStorage,
* which are vulnerable to extraction by malicious apps or scripts.
*
* ### Why use it?
* - Stores data encrypted and protected at OS level.
* - Prevents unauthorized access to tokens.
* - Essential for meeting security best practices in native mobile apps.
*
* ### Installation:
* In an Expo managed workflow:
* ```
* expo install expo-secure-store
* ```
* For bare React Native projects:
* - Follow installation and linking instructions here: 
*   https://docs.expo.dev/versions/latest/sdk/securestore/#bare-react-native-projects
*
* ### Documentation:
* - Official Expo docs: https://docs.expo.dev/versions/latest/sdk/securestore/
*
* ### Alternatives if not using Expo:
* - [`react-native-keychain`](https://github.com/oblador/react-native-keychain) — iOS/Android Keychain/Keystore integration
* - [`react-native-sensitive-info`](https://github.com/mCodex/react-native-sensitive-info) — cross-platform secure storage
*
* ### Important security notes:
* - On the **web**, you should *never* store JWTs in localStorage or sessionStorage due to XSS risks.
*   Instead, store tokens in **HttpOnly, Secure cookies** (as your current setup does).
* - On **native apps**, use secure storage (like `expo-secure-store`) to protect tokens.
* - This strategy helps you safely support both web (cookie-based) and native (secure store + auth header) authentication flows.
*/
//TODO - import * as SecureStore from 'expo-secure-store';


//TODO - const ACCESS_TOKEN_KEY = 'accessToken';
//TODO - const REFRESH_TOKEN_KEY = 'refreshToken';

/**
* Get the stored access token from secure storage.
*/
export async function getAccessTokenNative(): Promise<string | null>
{
	console.log('nativeTokenStorage: getAccessToken called');
	/*try
	{
		return await SecureStore.getItemAsync(ACCESS_TOKEN_KEY);
	}
	catch (error)
	{
		console.error('nativeTokenStorage: getAccessToken failed', error);
		
		return null;
	}*/
}

/**
* Store the access token securely.
*/
export async function setAccessTokenNative(token: string): Promise<void>
{
	console.log('nativeTokenStorage: setAccessToken called', token);
	/*try
	{
		await SecureStore.setItemAsync(ACCESS_TOKEN_KEY, token);
	}
	catch (error)
	{
		console.error('nativeTokenStorage: setAccessToken failed', error);
	}*/
}

/**
* Get the stored refresh token from secure storage.
*/
export async function getRefreshTokenNative(): Promise<string | null>
{
	console.log('nativeTokenStorage: getRefreshToken called');
	/*try
	{
		return await SecureStore.getItemAsync(REFRESH_TOKEN_KEY);
	}
	catch (error)
	{
		console.error('nativeTokenStorage: getRefreshToken failed', error);
		
		return null;
	}*/
}

/**
* Store the refresh token securely.
*/
export async function setRefreshTokenNative(token: string): Promise<void>
{
	console.log('nativeTokenStorage: setRefeshToken called', token);
	/*try
	{
		await SecureStore.setItemAsync(REFRESH_TOKEN_KEY, token);
	}
	catch (error)
	{
		console.error('nativeTokenStorage: setRefreshToken failed', error);
	}*/
}

/**
* Clear both access and refresh tokens from secure storage.
*/
export async function clearTokensNative(): Promise<void>
{
	console.log('nativeTokenStorage: clearTokens called');
	/*try
	{
		await SecureStore.deleteItemAsync(ACCESS_TOKEN_KEY);
		await SecureStore.deleteItemAsync(REFRESH_TOKEN_KEY);
	}
	catch (error)
	{
		console.error('nativeTokenStorage: clearTokens failed', error);
	}*/
}

```

Can you help with this?