fix: tab bar not visible on Android #21

Merged
Ichitux merged 5 commits from fix/android-tab-bar-visibility into main 2026-07-07 21:53:18 +00:00
5 changed files with 99 additions and 13 deletions
Showing only changes of commit 6612c8b10c - Show all commits
+7 -7
View File
@@ -417,12 +417,12 @@ export default function ProfileScreen() {
return ( return (
<ScrollView style={styles.container}> <ScrollView style={styles.container}>
{/* Avatar Section */} {/* Avatar Section */}
<View style={styles.avatarSection}> <View style={[styles.avatarSection, isTablet && styles.avatarSectionTablet]}>
<TouchableOpacity style={styles.avatarContainer} onPress={() => setShowAvatarModal(true)}> <TouchableOpacity style={[styles.avatarContainer, isTablet && styles.avatarContainerTablet]} onPress={() => setShowAvatarModal(true)}>
{avatarUrl ? ( {avatarUrl ? (
<Image source={{ uri: avatarUrl }} style={styles.avatarImage} /> <Image source={{ uri: avatarUrl }} style={[styles.avatarImage, isTablet && styles.avatarImageTablet]} />
) : ( ) : (
<Ionicons name="person" size={40} color={colors.primary} /> <Ionicons name="person" size={isTablet ? 50 : 40} color={colors.primary} />
)} )}
<View style={styles.avatarOverlay}> <View style={styles.avatarOverlay}>
<Ionicons name="camera" size={24} color="white" /> <Ionicons name="camera" size={24} color="white" />
@@ -432,7 +432,7 @@ export default function ProfileScreen() {
</View> </View>
{/* Read-only Name Section */} {/* Read-only Name Section */}
<View style={styles.infoSection}> <View style={[styles.infoSection, isTablet && styles.infoSectionTablet]}>
<View style={styles.infoCard}> <View style={styles.infoCard}>
<Text style={styles.infoLabel}>Nombre</Text> <Text style={styles.infoLabel}>Nombre</Text>
<Text style={styles.infoValue}>{firstName || '—'}</Text> <Text style={styles.infoValue}>{firstName || '—'}</Text>
@@ -444,7 +444,7 @@ export default function ProfileScreen() {
</View> </View>
{/* Menu Section */} {/* Menu Section */}
<View style={styles.menuSection}> <View style={[styles.menuSection, isTablet && styles.menuSectionTablet]}>
<TouchableOpacity style={styles.menuItem} onPress={openConfig}> <TouchableOpacity style={styles.menuItem} onPress={openConfig}>
<Ionicons name="settings" size={24} color={colors.textSecondary} /> <Ionicons name="settings" size={24} color={colors.textSecondary} />
<Text style={styles.menuText}>Configuración</Text> <Text style={styles.menuText}>Configuración</Text>
@@ -484,7 +484,7 @@ export default function ProfileScreen() {
</View> </View>
{/* Logout Button */} {/* Logout Button */}
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}> <TouchableOpacity style={[styles.logoutButton, isTablet && styles.logoutButtonTablet]} onPress={handleLogout}>
<Ionicons name="log-out" size={20} color={colors.danger} /> <Ionicons name="log-out" size={20} color={colors.danger} />
<Text style={styles.logoutText}>Cerrar Sesión</Text> <Text style={styles.logoutText}>Cerrar Sesión</Text>
</TouchableOpacity> </TouchableOpacity>
+13 -3
View File
@@ -9,6 +9,7 @@ import AdminView from './views/AdminView';
import LoginModal from './components/LoginModal'; import LoginModal from './components/LoginModal';
import SavedNotifications from './components/SavedNotifications'; import SavedNotifications from './components/SavedNotifications';
import BottomNav from './components/BottomNav'; import BottomNav from './components/BottomNav';
import { getFaro } from './utils/faro';
function App() { function App() {
const [screen, setScreen] = useState('home'); const [screen, setScreen] = useState('home');
@@ -40,7 +41,10 @@ function App() {
fetch('/api/auth/check') fetch('/api/auth/check')
.then(r => r.json()) .then(r => r.json())
.then(data => { if (data.authenticated) setCurrentUser(data.user); }) .then(data => { if (data.authenticated) setCurrentUser(data.user); })
.catch(() => {}) .catch((err) => {
console.warn('[auth/check]', err);
getFaro()?.pushError(err, { type: 'network', url: '/api/auth/check' });
})
.finally(() => setAuthChecked(true)); .finally(() => setAuthChecked(true));
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
@@ -57,7 +61,10 @@ function App() {
const count = (data.global?.length || 0) + (data.pharmacy?.length || 0); const count = (data.global?.length || 0) + (data.pharmacy?.length || 0);
setBadgeCount(count); setBadgeCount(count);
}) })
.catch(() => {}); .catch((err) => {
console.warn('[notifications/mine]', err);
getFaro()?.pushError(err, { type: 'network', url: '/api/notifications/mine' });
});
return () => { cancelled = true; }; return () => { cancelled = true; };
}, [currentUser]); }, [currentUser]);
@@ -69,7 +76,10 @@ function App() {
if (!data) return; if (!data) return;
setBadgeCount((data.global?.length || 0) + (data.pharmacy?.length || 0)); setBadgeCount((data.global?.length || 0) + (data.pharmacy?.length || 0));
}) })
.catch(() => {}); .catch((err) => {
console.warn('[refreshBadge]', err);
getFaro()?.pushError(err, { type: 'network', url: '/api/notifications/mine' });
});
} }
function handleLogin(user) { function handleLogin(user) {
@@ -0,0 +1,57 @@
import React from 'react';
export default class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
// Report to Faro if available
try {
const faro = window.__faro;
if (faro?.api?.pushError) {
faro.api.pushError(error, { type: 'react-boundary', componentStack: info.componentStack });
}
} catch {
// Faro reporting must never break the app
}
console.error('[ErrorBoundary]', error, info);
}
render() {
if (this.state.hasError) {
return (
<div style={{
padding: '2rem',
textAlign: 'center',
fontFamily: 'system-ui, sans-serif',
color: '#333',
}}>
<h2>Algo salió mal</h2>
<p style={{ color: '#666' }}>Ha ocurrido un error inesperado. Por favor, recarga la página.</p>
<button
onClick={() => window.location.reload()}
style={{
marginTop: '1rem',
padding: '0.5rem 1.5rem',
backgroundColor: '#27633a',
color: '#fff',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '1rem',
}}
>
Recargar
</button>
</div>
);
}
return this.props.children;
}
}
+12 -1
View File
@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import App from './App'; import App from './App';
import ErrorBoundary from './components/ErrorBoundary';
import './index.css'; import './index.css';
import { initNativeShell } from './utils/native'; import { initNativeShell } from './utils/native';
import { initFaro } from './utils/faro'; import { initFaro } from './utils/faro';
@@ -9,9 +10,19 @@ import { initFaro } from './utils/faro';
// No-op if VITE_FARO_ENDPOINT is not configured. // No-op if VITE_FARO_ENDPOINT is not configured.
initFaro(); initFaro();
// Global unhandled promise rejection handler — report to Faro
window.addEventListener('unhandledrejection', (event) => {
console.warn('[unhandledrejection]', event.reason);
try {
window.__faro?.api?.pushError(event.reason, { type: 'unhandled-rejection' });
} catch { /* Faro must never break the app */ }
});
ReactDOM.createRoot(document.getElementById('root')).render( ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode> <React.StrictMode>
<App /> <ErrorBoundary>
<App />
</ErrorBoundary>
</React.StrictMode> </React.StrictMode>
); );
+9 -1
View File
@@ -17,6 +17,11 @@ import { TracingInstrumentation } from '@grafana/faro-web-tracing';
import { OtlpHttpTransport } from '@grafana/faro-transport-otlp-http'; import { OtlpHttpTransport } from '@grafana/faro-transport-otlp-http';
let initialized = false; let initialized = false;
let faroApi = null;
export function getFaro() {
return faroApi;
}
export function initFaro() { export function initFaro() {
if (initialized) return; if (initialized) return;
@@ -34,7 +39,7 @@ export function initFaro() {
const version = import.meta.env.VITE_FARO_APP_VERSION || '1.0.0'; const version = import.meta.env.VITE_FARO_APP_VERSION || '1.0.0';
try { try {
initializeFaro({ const faro = initializeFaro({
url: `${endpoint.replace(/\/$/, '')}/v1/traces`, url: `${endpoint.replace(/\/$/, '')}/v1/traces`,
app: { app: {
name: appName, name: appName,
@@ -61,6 +66,9 @@ export function initFaro() {
url: `${endpoint.replace(/\/$/, '')}/v1/traces`, url: `${endpoint.replace(/\/$/, '')}/v1/traces`,
}), }),
}); });
faroApi = faro.api;
// Expose for ErrorBoundary and manual reporting
window.__faro = faro;
} catch (err) { } catch (err) {
// Never let Faro init failure break the app. // Never let Faro init failure break the app.
// eslint-disable-next-line no-console // eslint-disable-next-line no-console