fix: resolve 6 frontend-mobile build and runtime issues
Run Tests on Branches / Backend Tests (push) Successful in 1m56s
Run Tests on Branches / Frontend Tests (push) Successful in 1m54s

- Add missing User type fields (first_name, last_name, email, avatar_url,
  city, address) used in profile.tsx
- Fix expo-constants import to use namespace import (consistent with
  rest of codebase)
- Replace placeholder production API URL with configurable value
- Fix avatar preset/color selection passing require() IDs to Image uri
  by tracking local source separately
- Replace all relative fetch() calls with configured axios instance
  (relative URLs don't resolve in React Native)
This commit is contained in:
Antoni Nuñez Romeu
2026-07-07 16:07:15 +02:00
parent fc12582814
commit 2843b61f82
3 changed files with 54 additions and 110 deletions
+33 -95
View File
@@ -1,10 +1,11 @@
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Alert, TextInput, ScrollView, Image, ActivityIndicator, Modal, Pressable } from 'react-native';
import { View, Text, StyleSheet, TouchableOpacity, Alert, TextInput, ScrollView, Image, ActivityIndicator, Modal, Pressable, ImageSourcePropType } from 'react-native';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import { useAuth } from '../../hooks/useAuth';
import { colors, spacing, borderRadius } from '../../constants/theme';
import api from '../../services/api';
const AVATARS = [
require('../../assets/avatars/avatar1.png'),
@@ -49,9 +50,17 @@ export default function ProfileScreen() {
const [firstName, setFirstName] = useState(user?.first_name || '');
const [lastName, setLastName] = useState(user?.last_name || '');
const [avatarUrl, setAvatarUrl] = useState(user?.avatar_url || '');
const [avatarLocalSource, setAvatarLocalSource] = useState<number | null>(null);
const [searchHistory, setSearchHistory] = useState<SearchHistoryItem[]>([]);
function getAvatarSource(): ImageSourcePropType {
if (!avatarUrl) return { uri: '' };
const presetMatch = avatarUrl.match(/^preset_avatar_(\d+)$/);
if (presetMatch) return AVATARS[parseInt(presetMatch[1], 10) - 1];
const colorMatch = avatarUrl.match(/^color_circle_(\d+)$/);
if (colorMatch) return COLOR_CIRCLES[parseInt(colorMatch[1], 10) - 1];
return { uri: avatarUrl };
}
// Avatar modal state
const [showAvatarModal, setShowAvatarModal] = useState(false);
const [avatarTab, setAvatarTab] = useState<'presets' | 'colors' | 'upload'>('presets');
@@ -92,11 +101,8 @@ export default function ProfileScreen() {
async function loadSearchHistory() {
try {
const res = await fetch('/api/search-history', { credentials: 'include' });
if (res.ok) {
const data = await res.json();
const { data } = await api.get('/search-history');
setSearchHistory(data);
}
} catch (err) {
console.error('Error loading search history:', err);
}
@@ -116,29 +122,20 @@ export default function ProfileScreen() {
setConfigSaving(true);
setConfigFeedback(null);
try {
const res = await fetch('/api/users/me', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
const { data: updated } = await api.put('/users/me', {
first_name: configFirstName.trim() || null,
last_name: configLastName.trim() || null,
email: configEmail.trim() || null,
city: configCity.trim() || null,
address: configAddress.trim() || null,
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || 'Error al guardar');
}
const updated = await res.json();
setFirstName(updated.first_name || '');
setLastName(updated.last_name || '');
setConfigFeedback({ type: 'ok', text: 'Perfil guardado.' });
setTimeout(() => setShowConfig(false), 1200);
} catch (err: any) {
setConfigFeedback({ type: 'err', text: err.message || 'Error al guardar' });
const msg = err.response?.data?.error || err.message || 'Error al guardar';
setConfigFeedback({ type: 'err', text: msg });
} finally {
setConfigSaving(false);
}
@@ -156,16 +153,7 @@ export default function ProfileScreen() {
setAvatarUrl(result.assets[0].uri);
setShowAvatarModal(false);
try {
const res = await fetch('/api/users/me', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ avatar_url: result.assets[0].uri }),
});
if (res.ok) {
const updated = await res.json();
// Trigger auth refresh to update user state
}
await api.put('/users/me', { avatar_url: result.assets[0].uri });
} catch (err) {
console.error('Error saving avatar:', err);
}
@@ -189,15 +177,7 @@ export default function ProfileScreen() {
setAvatarUrl(result.assets[0].uri);
setShowAvatarModal(false);
try {
const res = await fetch('/api/users/me', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ avatar_url: result.assets[0].uri }),
});
if (res.ok) {
const updated = await res.json();
}
await api.put('/users/me', { avatar_url: result.assets[0].uri });
} catch (err) {
console.error('Error saving avatar:', err);
}
@@ -205,38 +185,22 @@ export default function ProfileScreen() {
}
async function handleSelectPresetAvatar(index: number) {
const avatarSource = AVATARS[index];
setAvatarUrl(avatarSource);
const avatarValue = `preset_avatar_${index + 1}`;
setAvatarUrl(avatarValue);
setShowAvatarModal(false);
try {
const res = await fetch('/api/users/me', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ avatar_url: `preset_avatar_${index + 1}` }),
});
if (res.ok) {
const updated = await res.json();
}
await api.put('/users/me', { avatar_url: avatarValue });
} catch (err) {
console.error('Error saving avatar:', err);
}
}
async function handleSelectColor(index: number) {
const colorSource = COLOR_CIRCLES[index];
setAvatarUrl(colorSource);
const colorValue = `color_circle_${index + 1}`;
setAvatarUrl(colorValue);
setShowAvatarModal(false);
try {
const res = await fetch('/api/users/me', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ avatar_url: `color_circle_${index + 1}` }),
});
if (res.ok) {
const updated = await res.json();
}
await api.put('/users/me', { avatar_url: colorValue });
} catch (err) {
console.error('Error saving avatar:', err);
}
@@ -244,13 +208,8 @@ export default function ProfileScreen() {
async function handleDeleteSearch(id: number) {
try {
const res = await fetch(`/api/search-history/${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (res.ok) {
await api.delete(`/search-history/${id}`);
setSearchHistory(prev => prev.filter(item => item.id !== id));
}
} catch (err) {
console.error('Error deleting search:', err);
}
@@ -260,11 +219,8 @@ export default function ProfileScreen() {
async function loadAddresses() {
setAddressesLoading(true);
try {
const res = await fetch('/api/addresses', { credentials: 'include' });
if (res.ok) {
const data = await res.json();
const { data } = await api.get('/addresses');
setAddresses(data);
}
} catch (err) {
console.error('Error loading addresses:', err);
} finally {
@@ -304,27 +260,19 @@ export default function ProfileScreen() {
setFormSaving(true);
setFormError('');
try {
const url = editingAddressId ? `/api/addresses/${editingAddressId}` : '/api/addresses';
const method = editingAddressId ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
const url = editingAddressId ? `/addresses/${editingAddressId}` : '/addresses';
const method = editingAddressId ? 'put' : 'post';
await api[method](url, {
address: addr,
label: formLabel.trim(),
is_default: formDefault,
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || 'Error al guardar la dirección');
}
setShowAddressForm(false);
setEditingAddressId(null);
loadAddresses();
} catch (err: any) {
setFormError(err.message);
const msg = err.response?.data?.error || err.message || 'Error al guardar la dirección';
setFormError(msg);
} finally {
setFormSaving(false);
}
@@ -332,13 +280,8 @@ export default function ProfileScreen() {
async function handleDeleteAddress(id: number) {
try {
const res = await fetch(`/api/addresses/${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (res.ok) {
await api.delete(`/addresses/${id}`);
setAddresses(prev => prev.filter(a => a.id !== id));
}
} catch (err) {
console.error('Error deleting address:', err);
}
@@ -346,13 +289,8 @@ export default function ProfileScreen() {
async function handleSetDefault(id: number) {
try {
const res = await fetch(`/api/addresses/${id}/default`, {
method: 'PUT',
credentials: 'include',
});
if (res.ok) {
await api.put(`/addresses/${id}/default`);
loadAddresses();
}
} catch (err) {
console.error('Error setting default address:', err);
}
@@ -416,7 +354,7 @@ export default function ProfileScreen() {
<View style={styles.avatarSection}>
<TouchableOpacity style={styles.avatarContainer} onPress={() => setShowAvatarModal(true)}>
{avatarUrl ? (
<Image source={{ uri: avatarUrl }} style={styles.avatarImage} />
<Image source={getAvatarSource()} style={styles.avatarImage} />
) : (
<Ionicons name="person" size={40} color={colors.primary} />
)}
+2 -2
View File
@@ -1,11 +1,11 @@
import Constants from 'expo-constants';
import * as Constants from 'expo-constants';
const ENV = {
development: {
API_BASE_URL: 'http://localhost:3001/api',
},
production: {
API_BASE_URL: 'https://your-production-api.com/api',
API_BASE_URL: Constants.expoConfig?.extra?.apiUrl || 'https://api.farmafinder.com/api',
},
};
+6
View File
@@ -32,6 +32,12 @@ export interface User {
id: number;
username: string;
is_admin: boolean;
first_name?: string;
last_name?: string;
email?: string;
avatar_url?: string;
city?: string;
address?: string;
}
export interface AuthResponse {