- 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.
398 lines
13 KiB
TypeScript
398 lines
13 KiB
TypeScript
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native';
|
|
import { WebView, WebViewNavigation } from 'react-native-webview';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
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();
|
|
const webViewRef = useRef<WebView>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [canGoBack, setCanGoBack] = useState(false);
|
|
const [authToken, setAuthToken] = useState<string | null>(null);
|
|
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 and check/refresh token if needed
|
|
const loadCredentials = useCallback(async () => {
|
|
try {
|
|
// 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);
|
|
}
|
|
}, []);
|
|
|
|
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
|
|
? `
|
|
(function() {
|
|
try {
|
|
var authData = {
|
|
username: '${userName || ''}',
|
|
token: '${authToken}',
|
|
user_id: ${userId || 'null'}
|
|
};
|
|
localStorage.setItem('auth2', JSON.stringify(authData));
|
|
console.log('Auth injected:', authData.username);
|
|
} catch(e) {
|
|
console.error('Failed to inject token:', e);
|
|
}
|
|
})();
|
|
true;
|
|
`
|
|
: '';
|
|
|
|
const handleRefresh = () => {
|
|
setError(null);
|
|
setIsLoading(true);
|
|
webViewRef.current?.reload();
|
|
};
|
|
|
|
const handleWebViewBack = () => {
|
|
if (canGoBack) {
|
|
webViewRef.current?.goBack();
|
|
}
|
|
};
|
|
|
|
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);
|
|
};
|
|
|
|
// Wait for token to load
|
|
if (!isTokenLoaded) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<View style={styles.header}>
|
|
<View>
|
|
<Text style={styles.greeting}>Hello, {user?.user_name || 'User'}</Text>
|
|
<Text style={styles.headerTitle}>Dashboard</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
<Text style={styles.loadingText}>Preparing dashboard...</Text>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<View style={styles.header}>
|
|
<View>
|
|
<Text style={styles.greeting}>Hello, {user?.user_name || 'User'}</Text>
|
|
<Text style={styles.headerTitle}>Dashboard</Text>
|
|
</View>
|
|
<TouchableOpacity style={styles.refreshButton} onPress={handleRefresh}>
|
|
<Ionicons name="refresh" size={22} color={AppColors.primary} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
<View style={styles.errorContainer}>
|
|
<Ionicons name="cloud-offline-outline" size={64} color={AppColors.textMuted} />
|
|
<Text style={styles.errorTitle}>Connection Error</Text>
|
|
<Text style={styles.errorText}>{error}</Text>
|
|
<TouchableOpacity style={styles.retryButton} onPress={handleRefresh}>
|
|
<Text style={styles.retryButtonText}>Try Again</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<View>
|
|
<Text style={styles.greeting}>Hello, {user?.user_name || 'User'}</Text>
|
|
<Text style={styles.headerTitle}>Dashboard</Text>
|
|
</View>
|
|
<View style={styles.headerActions}>
|
|
{canGoBack && (
|
|
<TouchableOpacity style={styles.actionButton} onPress={handleWebViewBack}>
|
|
<Ionicons name="chevron-back" size={22} color={AppColors.primary} />
|
|
</TouchableOpacity>
|
|
)}
|
|
<TouchableOpacity style={styles.actionButton} onPress={handleRefresh}>
|
|
<Ionicons name="refresh" size={22} color={AppColors.primary} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
{/* WebView Dashboard */}
|
|
<View style={styles.webViewContainer}>
|
|
<WebView
|
|
ref={webViewRef}
|
|
source={{ uri: DASHBOARD_URL }}
|
|
style={styles.webView}
|
|
onLoadStart={() => setIsLoading(true)}
|
|
onLoadEnd={() => setIsLoading(false)}
|
|
onError={handleError}
|
|
onHttpError={handleError}
|
|
onNavigationStateChange={handleNavigationStateChange}
|
|
onShouldStartLoadWithRequest={handleShouldStartLoadWithRequest}
|
|
javaScriptEnabled={true}
|
|
domStorageEnabled={true}
|
|
startInLoadingState={true}
|
|
scalesPageToFit={true}
|
|
allowsBackForwardNavigationGestures={true}
|
|
injectedJavaScriptBeforeContentLoaded={injectedJavaScript}
|
|
injectedJavaScript={injectedJavaScript}
|
|
renderLoading={() => (
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
<Text style={styles.loadingText}>Loading dashboard...</Text>
|
|
</View>
|
|
)}
|
|
/>
|
|
|
|
{isLoading && (
|
|
<View style={styles.loadingOverlay}>
|
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
</View>
|
|
)}
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: AppColors.background,
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
paddingHorizontal: Spacing.lg,
|
|
paddingVertical: Spacing.md,
|
|
backgroundColor: AppColors.background,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: AppColors.border,
|
|
},
|
|
greeting: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textSecondary,
|
|
},
|
|
headerTitle: {
|
|
fontSize: FontSizes.xl,
|
|
fontWeight: '700',
|
|
color: AppColors.textPrimary,
|
|
},
|
|
headerActions: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
actionButton: {
|
|
padding: Spacing.xs,
|
|
marginLeft: Spacing.xs,
|
|
},
|
|
refreshButton: {
|
|
padding: Spacing.xs,
|
|
},
|
|
webViewContainer: {
|
|
flex: 1,
|
|
},
|
|
webView: {
|
|
flex: 1,
|
|
},
|
|
loadingContainer: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: AppColors.background,
|
|
},
|
|
loadingOverlay: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: 'rgba(255,255,255,0.8)',
|
|
},
|
|
loadingText: {
|
|
marginTop: Spacing.md,
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textSecondary,
|
|
},
|
|
errorContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
padding: Spacing.xl,
|
|
},
|
|
errorTitle: {
|
|
fontSize: FontSizes.lg,
|
|
fontWeight: '600',
|
|
color: AppColors.textPrimary,
|
|
marginTop: Spacing.md,
|
|
},
|
|
errorText: {
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textSecondary,
|
|
textAlign: 'center',
|
|
marginTop: Spacing.xs,
|
|
},
|
|
retryButton: {
|
|
marginTop: Spacing.lg,
|
|
paddingHorizontal: Spacing.xl,
|
|
paddingVertical: Spacing.md,
|
|
backgroundColor: AppColors.primary,
|
|
borderRadius: 8,
|
|
},
|
|
retryButtonText: {
|
|
color: AppColors.white,
|
|
fontSize: FontSizes.base,
|
|
fontWeight: '600',
|
|
},
|
|
});
|