Fix Developer Mode WebView authentication

- Add legacy dashboard API methods (eluxnetworks.net)
- Implement JWT token validation before using cached credentials
- Clear invalid tokens (non-JWT strings like "0") and force re-login
- Use correct credentials (anandk/anandk_8)
- Add 30-minute token refresh interval when WebView is active
- Fix avatar upload using expo-file-system instead of FileReader
- Handle address field as both string and object
This commit is contained in:
Sergei 2026-01-09 17:06:35 -08:00
parent 24e7f057e7
commit 973e9b7ebe
2 changed files with 232 additions and 34 deletions

View File

@ -63,7 +63,12 @@ export default function BeneficiaryDetailScreen() {
const [error, setError] = useState<string | null>(null);
const [showWebView, setShowWebView] = useState(false);
const [isWebViewReady, setIsWebViewReady] = useState(false);
const [authToken, setAuthToken] = useState<string | null>(null);
const [legacyCredentials, setLegacyCredentials] = useState<{
token: string;
userName: string;
userId: string;
} | null>(null);
const [isRefreshingToken, setIsRefreshingToken] = useState(false);
// Edit modal state
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
@ -71,21 +76,67 @@ export default function BeneficiaryDetailScreen() {
const webViewRef = useRef<WebView>(null);
// Load legacy token for WebView dashboard
useEffect(() => {
const loadLegacyToken = async () => {
// Load legacy credentials for WebView dashboard
const loadLegacyCredentials = useCallback(async () => {
try {
const token = await api.getLegacyToken();
setAuthToken(token);
// Check if token is expiring soon
const isExpiring = await api.isLegacyTokenExpiringSoon();
if (isExpiring) {
console.log('[DevMode] Legacy token expiring, refreshing...');
await api.refreshLegacyToken();
}
const credentials = await api.getLegacyWebViewCredentials();
if (credentials) {
setLegacyCredentials(credentials);
console.log('[DevMode] Legacy credentials loaded:', credentials.userName);
}
setIsWebViewReady(true);
} catch (err) {
console.log('[BeneficiaryDetail] Legacy token not available');
console.log('[DevMode] Failed to load legacy credentials:', err);
setIsWebViewReady(true);
}
};
loadLegacyToken();
}, []);
useEffect(() => {
loadLegacyCredentials();
// Periodically refresh token (every 30 minutes)
const tokenCheckInterval = setInterval(async () => {
if (!showWebView) return; // Only refresh if WebView is active
const isExpiring = await api.isLegacyTokenExpiringSoon();
if (isExpiring && !isRefreshingToken) {
console.log('[DevMode] Periodic check: refreshing legacy token...');
setIsRefreshingToken(true);
const result = await api.refreshLegacyToken();
if (result.ok) {
const credentials = await api.getLegacyWebViewCredentials();
if (credentials) {
setLegacyCredentials(credentials);
// 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);
}
}
setIsRefreshingToken(false);
}
}, 30 * 60 * 1000); // 30 minutes
return () => clearInterval(tokenCheckInterval);
}, [loadLegacyCredentials, showWebView, isRefreshingToken]);
const loadBeneficiary = useCallback(async (showLoadingIndicator = true) => {
if (!id) return;
@ -241,15 +292,18 @@ export default function BeneficiaryDetailScreen() {
};
// JavaScript to inject token into localStorage for WebView
const injectedJavaScript = authToken
// Web app expects auth2 as JSON: {username, token, user_id}
const injectedJavaScript = legacyCredentials
? `
(function() {
try {
var authData = {
token: '${authToken}'
username: '${legacyCredentials.userName}',
token: '${legacyCredentials.token}',
user_id: ${legacyCredentials.userId}
};
localStorage.setItem('auth2', JSON.stringify(authData));
console.log('Auth data injected');
console.log('Auth data injected:', authData.username);
} catch(e) {
console.error('Failed to inject token:', e);
}
@ -343,10 +397,10 @@ export default function BeneficiaryDetailScreen() {
{/* Content area - WebView or MockDashboard */}
<View style={styles.dashboardContent}>
{showWebView ? (
isWebViewReady && authToken ? (
isWebViewReady && legacyCredentials ? (
<WebView
ref={webViewRef}
source={{ uri: getDashboardUrl(FERDINAND_DEPLOYMENT_ID) }}
source={{ uri: getDashboardUrl(api.getDemoDeploymentId()) }}
style={styles.webView}
javaScriptEnabled={true}
domStorageEnabled={true}
@ -368,7 +422,7 @@ export default function BeneficiaryDetailScreen() {
<View style={styles.webViewLoading}>
<ActivityIndicator size="large" color={AppColors.primary} />
<Text style={styles.webViewLoadingText}>
{isWebViewReady ? 'Loading dashboard...' : 'Connecting to sensors...'}
{isWebViewReady ? 'Authenticating...' : 'Connecting to sensors...'}
</Text>
</View>
)

View File

@ -1,5 +1,6 @@
import type { ApiError, ApiResponse, AuthResponse, Beneficiary, BeneficiaryDashboardData, ChatResponse, DashboardSingleResponse, NotificationSettings } from '@/types';
import * as Crypto from 'expo-crypto';
import * as FileSystem from 'expo-file-system';
import * as SecureStore from 'expo-secure-store';
// Callback for handling unauthorized responses (401)
@ -609,10 +610,10 @@ class ApiService {
const beneficiaries: Beneficiary[] = (data.beneficiaries || []).map((item: any) => ({
id: item.id,
name: item.name || item.email,
avatar: undefined, // No auto-generated avatars - only show if user uploaded one
avatar: item.avatarUrl || undefined, // Use uploaded avatar from server
status: 'offline' as const,
email: item.email,
address: item.address?.street ? `${item.address.street}, ${item.address.city}` : undefined,
address: typeof item.address === 'string' ? item.address : (item.address?.street ? `${item.address.street}, ${item.address.city}` : undefined),
subscription: item.subscription,
// Equipment status from orders
equipmentStatus: item.equipmentStatus,
@ -660,10 +661,10 @@ class ApiService {
const beneficiary: Beneficiary = {
id: data.id,
name: data.name || data.email,
avatar: undefined, // No auto-generated avatars - only show if user uploaded one
avatar: data.avatarUrl || undefined,
status: 'offline' as const,
email: data.email,
address: data.address?.street ? `${data.address.street}, ${data.address.city}` : undefined,
address: typeof data.address === 'string' ? data.address : (data.address?.street ? `${data.address.street}, ${data.address.city}` : undefined),
subscription: data.subscription ? {
status: data.subscription.status,
plan: data.subscription.plan,
@ -775,21 +776,23 @@ class ApiService {
let base64Image: string | null = null;
if (imageUri) {
// Convert file URI to base64
const response = await fetch(imageUri);
const blob = await response.blob();
// Convert blob to base64
const base64 = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(blob);
// Read file as base64 string using expo-file-system
const base64Data = await FileSystem.readAsStringAsync(imageUri, {
encoding: FileSystem.EncodingType.Base64,
});
base64Image = base64;
// Determine mime type from URI extension
const extension = imageUri.split('.').pop()?.toLowerCase() || 'jpeg';
const mimeType = extension === 'png' ? 'image/png' : 'image/jpeg';
// Create data URI
base64Image = `data:${mimeType};base64,${base64Data}`;
console.log('[API] Avatar converted to base64, length:', base64Image.length);
}
console.log('[API] Uploading avatar for beneficiary:', id);
const apiResponse = await fetch(`${WELLNUO_API_URL}/me/beneficiaries/${id}/avatar`, {
method: 'PATCH',
headers: {
@ -801,6 +804,8 @@ class ApiService {
const data = await apiResponse.json();
console.log('[API] Avatar upload response:', apiResponse.status, data);
if (!apiResponse.ok) {
return { ok: false, error: { message: data.error || 'Failed to update avatar' } };
}
@ -1284,6 +1289,145 @@ class ApiService {
};
}
}
// ==========================================
// Legacy Dashboard Methods (Developer Mode)
// For eluxnetworks.net dashboard WebView
// ==========================================
// Demo credentials for legacy dashboard
private readonly DEMO_LEGACY_USER = 'anandk';
private readonly DEMO_LEGACY_PASSWORD = 'anandk_8';
private readonly DEMO_DEPLOYMENT_ID = 21; // Ferdinand's deployment
// Login to legacy dashboard API
async loginToLegacyDashboard(): Promise<ApiResponse<AuthResponse>> {
try {
const formData = new URLSearchParams();
formData.append('function', 'credentials');
formData.append('user_name', this.DEMO_LEGACY_USER);
formData.append('ps', this.DEMO_LEGACY_PASSWORD);
formData.append('clientId', CLIENT_ID);
formData.append('nonce', this.generateNonce());
const response = await fetch(API_BASE_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData.toString(),
});
const data = await response.json();
console.log('[API] Legacy login response:', data.status, 'token type:', typeof data.access_token);
// Check that access_token is a valid JWT string (not 0 or empty)
if (data.status === '200 OK' && data.access_token && typeof data.access_token === 'string' && data.access_token.includes('.')) {
// Save legacy credentials
await SecureStore.setItemAsync('legacyAccessToken', data.access_token);
await SecureStore.setItemAsync('legacyUserId', String(data.user_id));
await SecureStore.setItemAsync('legacyUserName', this.DEMO_LEGACY_USER);
console.log('[API] Legacy credentials saved successfully');
return { data: data as AuthResponse, ok: true };
}
console.log('[API] Legacy login failed - invalid token:', data.access_token);
return {
ok: false,
error: { message: data.message || 'Legacy login failed - invalid credentials' },
};
} catch (error) {
console.error('[API] Legacy login error:', error);
return {
ok: false,
error: { message: 'Failed to connect to dashboard API' },
};
}
}
// Refresh legacy token
async refreshLegacyToken(): Promise<ApiResponse<AuthResponse>> {
console.log('[API] Refreshing legacy token...');
return this.loginToLegacyDashboard();
}
// Check if legacy token is expiring soon (within 1 hour)
async isLegacyTokenExpiringSoon(): Promise<boolean> {
try {
const token = await this.getLegacyToken();
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;
const isExpiring = (exp - now) < oneHour;
console.log('[API] Legacy token expiring soon:', isExpiring, 'expires in:', Math.round((exp - now) / 60), 'min');
return isExpiring;
} catch (e) {
console.log('[API] Error checking legacy token:', e);
return true;
}
}
// Get legacy credentials for WebView injection
async getLegacyWebViewCredentials(): Promise<{
token: string;
userName: string;
userId: string;
} | null> {
try {
const token = await SecureStore.getItemAsync('legacyAccessToken');
const userName = await SecureStore.getItemAsync('legacyUserName');
const userId = await SecureStore.getItemAsync('legacyUserId');
// Check if credentials exist AND token is valid JWT (contains dots)
const isValidToken = token && typeof token === 'string' && token.includes('.');
if (!isValidToken || !userName || !userId) {
console.log('[API] Legacy credentials missing or invalid token, logging in...');
console.log('[API] Token valid:', isValidToken, 'userName:', !!userName, 'userId:', !!userId);
// Clear any invalid cached credentials
if (token && !isValidToken) {
console.log('[API] Clearing invalid cached token:', token);
await SecureStore.deleteItemAsync('legacyAccessToken');
await SecureStore.deleteItemAsync('legacyUserName');
await SecureStore.deleteItemAsync('legacyUserId');
}
const loginResult = await this.loginToLegacyDashboard();
if (!loginResult.ok) return null;
// Get freshly saved credentials
const newToken = await SecureStore.getItemAsync('legacyAccessToken');
const newUserName = await SecureStore.getItemAsync('legacyUserName');
const newUserId = await SecureStore.getItemAsync('legacyUserId');
if (!newToken || !newUserName || !newUserId) return null;
return { token: newToken, userName: newUserName, userId: newUserId };
}
console.log('[API] Legacy credentials found:', userName, 'token length:', token.length);
return { token, userName, userId };
} catch (e) {
console.error('[API] Error getting legacy credentials:', e);
return null;
}
}
// Get demo deployment ID
getDemoDeploymentId(): number {
return this.DEMO_DEPLOYMENT_ID;
}
}
export const api = new ApiService();