diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index e1fe67d..18fd883 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -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(null); const [userId, setUserId] = useState(null); const [isTokenLoaded, setIsTokenLoaded] = useState(false); + const [isRefreshingToken, setIsRefreshingToken] = useState(false); - // Load credentials from SecureStore - useEffect(() => { - const loadCredentials = 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 }); - } catch (err) { - console.error('Failed to load credentials:', err); - } finally { - setIsTokenLoaded(true); + // 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'); + } } - }; - 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 // 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} diff --git a/metro.config.js b/metro.config.js new file mode 100644 index 0000000..a613618 --- /dev/null +++ b/metro.config.js @@ -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; diff --git a/services/api.ts b/services/api.ts index 00f031b..8c1a543 100644 --- a/services/api.ts +++ b/services/api.ts @@ -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> { + 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 { + 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 { await SecureStore.deleteItemAsync('accessToken'); await SecureStore.deleteItemAsync('userId'); await SecureStore.deleteItemAsync('userName'); + await SecureStore.deleteItemAsync('userPassword'); await SecureStore.deleteItemAsync('privileges'); await SecureStore.deleteItemAsync('maxRole'); }