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 { WebView } from 'react-native-webview';
|
||||
import { WebView, WebViewNavigation } from 'react-native-webview';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { api } from '@/services/api';
|
||||
import { AppColors, FontSizes, Spacing } from '@/constants/theme';
|
||||
|
||||
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() {
|
||||
const { user } = useAuth();
|
||||
@ -19,27 +21,119 @@ export default function HomeScreen() {
|
||||
const [userName, setUserName] = useState<string | null>(null);
|
||||
const [userId, setUserId] = useState<string | null>(null);
|
||||
const [isTokenLoaded, setIsTokenLoaded] = useState(false);
|
||||
const [isRefreshingToken, setIsRefreshingToken] = useState(false);
|
||||
|
||||
// Load credentials from SecureStore
|
||||
useEffect(() => {
|
||||
const loadCredentials = async () => {
|
||||
// Load credentials and check/refresh token if needed
|
||||
const loadCredentials = useCallback(async () => {
|
||||
try {
|
||||
const token = await SecureStore.getItemAsync('accessToken');
|
||||
const user = await SecureStore.getItemAsync('userName');
|
||||
const uid = await SecureStore.getItemAsync('userId');
|
||||
setAuthToken(token);
|
||||
setUserName(user);
|
||||
setUserId(uid);
|
||||
console.log('Home: Loaded credentials for WebView:', { hasToken: !!token, user, uid });
|
||||
// Check if token is expiring soon and refresh if needed
|
||||
const isExpiring = await api.isTokenExpiringSoon();
|
||||
if (isExpiring) {
|
||||
console.log('Token expiring soon, refreshing...');
|
||||
const refreshResult = await api.refreshToken();
|
||||
if (!refreshResult.ok) {
|
||||
console.log('Token refresh failed, using existing credentials');
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
loadCredentials();
|
||||
}, []);
|
||||
|
||||
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
|
||||
// Web app expects auth2 as JSON: {username, token, user_id}
|
||||
const injectedJavaScript = authToken
|
||||
@ -73,10 +167,33 @@ export default function HomeScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigationStateChange = (navState: any) => {
|
||||
const handleNavigationStateChange = (navState: WebViewNavigation) => {
|
||||
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 = () => {
|
||||
setError('Failed to load dashboard. Please check your internet connection.');
|
||||
setIsLoading(false);
|
||||
@ -155,6 +272,7 @@ export default function HomeScreen() {
|
||||
onError={handleError}
|
||||
onHttpError={handleError}
|
||||
onNavigationStateChange={handleNavigationStateChange}
|
||||
onShouldStartLoadWithRequest={handleShouldStartLoadWithRequest}
|
||||
javaScriptEnabled={true}
|
||||
domStorageEnabled={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) {
|
||||
// Save credentials to SecureStore
|
||||
// Save credentials to SecureStore (including password for auto-refresh)
|
||||
await SecureStore.setItemAsync('accessToken', response.data.access_token);
|
||||
await SecureStore.setItemAsync('userId', response.data.user_id.toString());
|
||||
await SecureStore.setItemAsync('userName', username);
|
||||
await SecureStore.setItemAsync('userPassword', password); // Store for token refresh
|
||||
await SecureStore.setItemAsync('privileges', response.data.privileges);
|
||||
await SecureStore.setItemAsync('maxRole', response.data.max_role.toString());
|
||||
}
|
||||
@ -84,10 +85,73 @@ class ApiService {
|
||||
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> {
|
||||
await SecureStore.deleteItemAsync('accessToken');
|
||||
await SecureStore.deleteItemAsync('userId');
|
||||
await SecureStore.deleteItemAsync('userName');
|
||||
await SecureStore.deleteItemAsync('userPassword');
|
||||
await SecureStore.deleteItemAsync('privileges');
|
||||
await SecureStore.deleteItemAsync('maxRole');
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user