Fix WebView session expiration with auto-refresh token
- Add refreshToken() method to automatically refresh expired tokens - Add isTokenExpiringSoon() to check if token expires within 1 hour - Store password in SecureStore for auto-refresh capability - Add periodic token check every 30 minutes in WebView - Intercept WebView navigation to login pages and refresh instead - Re-inject fresh token into WebView localStorage after refresh - Add metro.config.js to fix Metro bundler path resolution This prevents the app from showing web login page when session expires.
This commit is contained in:
parent
5e550f0f2b
commit
8c0be34f65
@ -1,13 +1,15 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native';
|
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native';
|
||||||
import { WebView } from 'react-native-webview';
|
import { WebView, WebViewNavigation } from 'react-native-webview';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import * as SecureStore from 'expo-secure-store';
|
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
|
import { api } from '@/services/api';
|
||||||
import { AppColors, FontSizes, Spacing } from '@/constants/theme';
|
import { AppColors, FontSizes, Spacing } from '@/constants/theme';
|
||||||
|
|
||||||
const DASHBOARD_URL = 'https://react.eluxnetworks.net/dashboard';
|
const DASHBOARD_URL = 'https://react.eluxnetworks.net/dashboard';
|
||||||
|
// URLs that indicate session expired (login page)
|
||||||
|
const LOGIN_URL_PATTERNS = ['/login', '/auth', '/signin'];
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@ -19,27 +21,119 @@ export default function HomeScreen() {
|
|||||||
const [userName, setUserName] = useState<string | null>(null);
|
const [userName, setUserName] = useState<string | null>(null);
|
||||||
const [userId, setUserId] = useState<string | null>(null);
|
const [userId, setUserId] = useState<string | null>(null);
|
||||||
const [isTokenLoaded, setIsTokenLoaded] = useState(false);
|
const [isTokenLoaded, setIsTokenLoaded] = useState(false);
|
||||||
|
const [isRefreshingToken, setIsRefreshingToken] = useState(false);
|
||||||
|
|
||||||
// Load credentials from SecureStore
|
// Load credentials and check/refresh token if needed
|
||||||
useEffect(() => {
|
const loadCredentials = useCallback(async () => {
|
||||||
const loadCredentials = async () => {
|
try {
|
||||||
try {
|
// Check if token is expiring soon and refresh if needed
|
||||||
const token = await SecureStore.getItemAsync('accessToken');
|
const isExpiring = await api.isTokenExpiringSoon();
|
||||||
const user = await SecureStore.getItemAsync('userName');
|
if (isExpiring) {
|
||||||
const uid = await SecureStore.getItemAsync('userId');
|
console.log('Token expiring soon, refreshing...');
|
||||||
setAuthToken(token);
|
const refreshResult = await api.refreshToken();
|
||||||
setUserName(user);
|
if (!refreshResult.ok) {
|
||||||
setUserId(uid);
|
console.log('Token refresh failed, using existing credentials');
|
||||||
console.log('Home: Loaded credentials for WebView:', { hasToken: !!token, user, uid });
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load credentials:', err);
|
|
||||||
} finally {
|
|
||||||
setIsTokenLoaded(true);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
loadCredentials();
|
// Get credentials (possibly refreshed)
|
||||||
|
const credentials = await api.getWebViewCredentials();
|
||||||
|
if (credentials) {
|
||||||
|
setAuthToken(credentials.token);
|
||||||
|
setUserName(credentials.userName);
|
||||||
|
setUserId(credentials.userId);
|
||||||
|
console.log('Home: Loaded credentials for WebView:', {
|
||||||
|
hasToken: !!credentials.token,
|
||||||
|
user: credentials.userName,
|
||||||
|
uid: credentials.userId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load credentials:', err);
|
||||||
|
} finally {
|
||||||
|
setIsTokenLoaded(true);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadCredentials();
|
||||||
|
|
||||||
|
// Periodically check and refresh token (every 30 minutes)
|
||||||
|
const tokenCheckInterval = setInterval(async () => {
|
||||||
|
const isExpiring = await api.isTokenExpiringSoon();
|
||||||
|
if (isExpiring) {
|
||||||
|
console.log('Periodic check: Token expiring, refreshing...');
|
||||||
|
const result = await api.refreshToken();
|
||||||
|
if (result.ok) {
|
||||||
|
const credentials = await api.getWebViewCredentials();
|
||||||
|
if (credentials) {
|
||||||
|
setAuthToken(credentials.token);
|
||||||
|
// Re-inject token into WebView
|
||||||
|
const injectScript = `
|
||||||
|
(function() {
|
||||||
|
var authData = {
|
||||||
|
username: '${credentials.userName}',
|
||||||
|
token: '${credentials.token}',
|
||||||
|
user_id: ${credentials.userId}
|
||||||
|
};
|
||||||
|
localStorage.setItem('auth2', JSON.stringify(authData));
|
||||||
|
console.log('Token auto-refreshed');
|
||||||
|
})();
|
||||||
|
true;
|
||||||
|
`;
|
||||||
|
webViewRef.current?.injectJavaScript(injectScript);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 30 * 60 * 1000); // 30 minutes
|
||||||
|
|
||||||
|
return () => clearInterval(tokenCheckInterval);
|
||||||
|
}, [loadCredentials]);
|
||||||
|
|
||||||
|
// Handle token refresh when WebView detects session expired
|
||||||
|
const handleTokenRefresh = useCallback(async () => {
|
||||||
|
if (isRefreshingToken) return;
|
||||||
|
|
||||||
|
setIsRefreshingToken(true);
|
||||||
|
console.log('WebView session expired, refreshing token...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const refreshResult = await api.refreshToken();
|
||||||
|
if (refreshResult.ok) {
|
||||||
|
// Get new credentials
|
||||||
|
const credentials = await api.getWebViewCredentials();
|
||||||
|
if (credentials) {
|
||||||
|
setAuthToken(credentials.token);
|
||||||
|
setUserName(credentials.userName);
|
||||||
|
setUserId(credentials.userId);
|
||||||
|
|
||||||
|
// Inject new token and reload
|
||||||
|
const injectScript = `
|
||||||
|
(function() {
|
||||||
|
var authData = {
|
||||||
|
username: '${credentials.userName}',
|
||||||
|
token: '${credentials.token}',
|
||||||
|
user_id: ${credentials.userId}
|
||||||
|
};
|
||||||
|
localStorage.setItem('auth2', JSON.stringify(authData));
|
||||||
|
console.log('Auth refreshed:', authData.username);
|
||||||
|
window.location.href = '${DASHBOARD_URL}';
|
||||||
|
})();
|
||||||
|
true;
|
||||||
|
`;
|
||||||
|
webViewRef.current?.injectJavaScript(injectScript);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Token refresh failed');
|
||||||
|
setError('Session expired. Please restart the app.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error refreshing token:', err);
|
||||||
|
} finally {
|
||||||
|
setIsRefreshingToken(false);
|
||||||
|
}
|
||||||
|
}, [isRefreshingToken]);
|
||||||
|
|
||||||
// JavaScript to inject auth token into localStorage
|
// JavaScript to inject auth token into localStorage
|
||||||
// Web app expects auth2 as JSON: {username, token, user_id}
|
// Web app expects auth2 as JSON: {username, token, user_id}
|
||||||
const injectedJavaScript = authToken
|
const injectedJavaScript = authToken
|
||||||
@ -73,10 +167,33 @@ export default function HomeScreen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNavigationStateChange = (navState: any) => {
|
const handleNavigationStateChange = (navState: WebViewNavigation) => {
|
||||||
setCanGoBack(navState.canGoBack);
|
setCanGoBack(navState.canGoBack);
|
||||||
|
|
||||||
|
// Check if WebView is trying to navigate to login page (session expired)
|
||||||
|
const url = navState.url?.toLowerCase() || '';
|
||||||
|
const isLoginPage = LOGIN_URL_PATTERNS.some(pattern => url.includes(pattern));
|
||||||
|
|
||||||
|
if (isLoginPage && !isRefreshingToken) {
|
||||||
|
console.log('Detected navigation to login page, refreshing token...');
|
||||||
|
handleTokenRefresh();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Intercept navigation requests to prevent loading login page
|
||||||
|
const handleShouldStartLoadWithRequest = useCallback((request: { url: string }) => {
|
||||||
|
const url = request.url?.toLowerCase() || '';
|
||||||
|
const isLoginPage = LOGIN_URL_PATTERNS.some(pattern => url.includes(pattern));
|
||||||
|
|
||||||
|
if (isLoginPage) {
|
||||||
|
console.log('Blocking navigation to login page, refreshing token instead');
|
||||||
|
handleTokenRefresh();
|
||||||
|
return false; // Block the navigation
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // Allow all other navigations
|
||||||
|
}, [handleTokenRefresh]);
|
||||||
|
|
||||||
const handleError = () => {
|
const handleError = () => {
|
||||||
setError('Failed to load dashboard. Please check your internet connection.');
|
setError('Failed to load dashboard. Please check your internet connection.');
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -155,6 +272,7 @@ export default function HomeScreen() {
|
|||||||
onError={handleError}
|
onError={handleError}
|
||||||
onHttpError={handleError}
|
onHttpError={handleError}
|
||||||
onNavigationStateChange={handleNavigationStateChange}
|
onNavigationStateChange={handleNavigationStateChange}
|
||||||
|
onShouldStartLoadWithRequest={handleShouldStartLoadWithRequest}
|
||||||
javaScriptEnabled={true}
|
javaScriptEnabled={true}
|
||||||
domStorageEnabled={true}
|
domStorageEnabled={true}
|
||||||
startInLoadingState={true}
|
startInLoadingState={true}
|
||||||
|
|||||||
8
metro.config.js
Normal file
8
metro.config.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const { getDefaultConfig } = require('expo/metro-config');
|
||||||
|
|
||||||
|
const config = getDefaultConfig(__dirname);
|
||||||
|
|
||||||
|
// Explicitly set projectRoot to prevent Metro from looking at parent directories
|
||||||
|
config.projectRoot = __dirname;
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
@ -73,10 +73,11 @@ class ApiService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok && response.data) {
|
if (response.ok && response.data) {
|
||||||
// Save credentials to SecureStore
|
// Save credentials to SecureStore (including password for auto-refresh)
|
||||||
await SecureStore.setItemAsync('accessToken', response.data.access_token);
|
await SecureStore.setItemAsync('accessToken', response.data.access_token);
|
||||||
await SecureStore.setItemAsync('userId', response.data.user_id.toString());
|
await SecureStore.setItemAsync('userId', response.data.user_id.toString());
|
||||||
await SecureStore.setItemAsync('userName', username);
|
await SecureStore.setItemAsync('userName', username);
|
||||||
|
await SecureStore.setItemAsync('userPassword', password); // Store for token refresh
|
||||||
await SecureStore.setItemAsync('privileges', response.data.privileges);
|
await SecureStore.setItemAsync('privileges', response.data.privileges);
|
||||||
await SecureStore.setItemAsync('maxRole', response.data.max_role.toString());
|
await SecureStore.setItemAsync('maxRole', response.data.max_role.toString());
|
||||||
}
|
}
|
||||||
@ -84,10 +85,73 @@ class ApiService {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh token using stored credentials
|
||||||
|
async refreshToken(): Promise<ApiResponse<AuthResponse>> {
|
||||||
|
try {
|
||||||
|
const userName = await SecureStore.getItemAsync('userName');
|
||||||
|
const password = await SecureStore.getItemAsync('userPassword');
|
||||||
|
|
||||||
|
if (!userName || !password) {
|
||||||
|
return { ok: false, error: { message: 'No stored credentials', code: 'NO_CREDENTIALS' } };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Refreshing token for user:', userName);
|
||||||
|
return await this.login(userName, password);
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: { message: 'Failed to refresh token', code: 'REFRESH_ERROR' }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is about to expire (within 1 hour)
|
||||||
|
async isTokenExpiringSoon(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const token = await this.getToken();
|
||||||
|
if (!token) return true;
|
||||||
|
|
||||||
|
// Decode JWT to get expiration
|
||||||
|
const parts = token.split('.');
|
||||||
|
if (parts.length !== 3) return true;
|
||||||
|
|
||||||
|
const payload = JSON.parse(atob(parts[1]));
|
||||||
|
const exp = payload.exp;
|
||||||
|
if (!exp) return true;
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const oneHour = 60 * 60;
|
||||||
|
|
||||||
|
return (exp - now) < oneHour;
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all credentials for WebView injection
|
||||||
|
async getWebViewCredentials(): Promise<{
|
||||||
|
token: string;
|
||||||
|
userName: string;
|
||||||
|
userId: string;
|
||||||
|
} | null> {
|
||||||
|
try {
|
||||||
|
const token = await SecureStore.getItemAsync('accessToken');
|
||||||
|
const userName = await SecureStore.getItemAsync('userName');
|
||||||
|
const userId = await SecureStore.getItemAsync('userId');
|
||||||
|
|
||||||
|
if (!token || !userName || !userId) return null;
|
||||||
|
|
||||||
|
return { token, userName, userId };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async logout(): Promise<void> {
|
async logout(): Promise<void> {
|
||||||
await SecureStore.deleteItemAsync('accessToken');
|
await SecureStore.deleteItemAsync('accessToken');
|
||||||
await SecureStore.deleteItemAsync('userId');
|
await SecureStore.deleteItemAsync('userId');
|
||||||
await SecureStore.deleteItemAsync('userName');
|
await SecureStore.deleteItemAsync('userName');
|
||||||
|
await SecureStore.deleteItemAsync('userPassword');
|
||||||
await SecureStore.deleteItemAsync('privileges');
|
await SecureStore.deleteItemAsync('privileges');
|
||||||
await SecureStore.deleteItemAsync('maxRole');
|
await SecureStore.deleteItemAsync('maxRole');
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user