5 Commits

Author SHA1 Message Date
Ichitux c8b830017b Merge branch 'main' into fix/mobile-view
Run Tests on Branches / Detect Changes (push) Successful in 14s
Run Tests on Branches / Backend Tests (push) Has been skipped
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
Run Tests on Branches / Frontend Tests (push) Has been skipped
Run Tests on Branches / Frontend Mobile Tests (push) Successful in 1m53s
2026-07-23 12:21:11 +00:00
Ichitux e641a35b82 Merge branch 'main' into fix/mobile-view
Run Tests on Branches / Detect Changes (push) Failing after 31s
Run Tests on Branches / Backend Tests (push) Has been skipped
Run Tests on Branches / Frontend Tests (push) Has been skipped
Run Tests on Branches / Frontend Mobile Tests (push) Has been skipped
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
2026-07-22 09:57:58 +00:00
Ichitux ff36748634 Merge branch 'main' into fix/mobile-view
Run Tests on Branches / Detect Changes (push) Successful in 11s
Run Tests on Branches / Backend Tests (push) Has been skipped
Run Tests on Branches / Frontend Tests (push) Has been skipped
Run Tests on Branches / Frontend Mobile Tests (push) Successful in 1m45s
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
2026-07-21 13:11:15 +00:00
Ichitux 795d935413 Remove Google Maps vs OSM
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
Run Tests on Branches / Detect Changes (push) Successful in 13s
Run Tests on Branches / Backend Tests (push) Has been skipped
Run Tests on Branches / Frontend Tests (push) Has been skipped
Run Tests on Branches / Frontend Mobile Tests (push) Successful in 1m35s
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
2026-07-17 16:11:10 +00:00
Ichitux 2f24dd4ff8 Faro & OSM
Run Tests on Branches / Detect Changes (push) Successful in 11s
Run Tests on Branches / Backend Tests (push) Has been skipped
Run Tests on Branches / Frontend Tests (push) Has been skipped
Run Tests on Branches / Frontend Mobile Tests (push) Successful in 1m53s
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
2026-07-17 15:58:46 +00:00
10 changed files with 77 additions and 251 deletions
-5
View File
@@ -6,8 +6,3 @@ EXPO_PUBLIC_API_URL=http://localhost:3001/api
# For production builds, update this to: # For production builds, update this to:
# EXPO_PUBLIC_API_URL=https://api.yourdomain.com/api # EXPO_PUBLIC_API_URL=https://api.yourdomain.com/api
# Grafana Alloy OTLP/HTTP endpoint for Faro RUM (must be reachable from the
# device — use a LAN IP / public hostname, NOT localhost).
# Open router :4318 → srv84-macos:4318, or proxy /faro via Nginx Proxy Manager.
EXPO_PUBLIC_FARO_URL=http://grafana.hacecalor.net:4318
+1 -7
View File
@@ -24,13 +24,7 @@
"foregroundImage": "./assets/adaptive-icon.png", "foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#007AFF" "backgroundColor": "#007AFF"
}, },
"package": "com.farmafinder.app", "package": "com.farmafinder.app"
"googleServicesFile": "./google-services.json",
"config": {
"googleMaps": {
"apiKey": ""
}
}
}, },
"plugins": [ "plugins": [
"expo-router", "expo-router",
+8 -1
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { View, StyleSheet, Text } from 'react-native'; import { View, StyleSheet, Text } from 'react-native';
import MapView, { Marker } from 'react-native-maps'; import MapView, { Marker, UrlTile } from 'react-native-maps';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { getPharmacies } from '../../services/pharmacies'; import { getPharmacies } from '../../services/pharmacies';
import { LoadingSpinner } from '../../components/LoadingSpinner'; import { LoadingSpinner } from '../../components/LoadingSpinner';
@@ -54,11 +54,18 @@ export default function MapScreen() {
<View style={styles.container}> <View style={styles.container}>
<MapView <MapView
style={styles.map} style={styles.map}
mapType="none"
region={region} region={region}
onRegionChangeComplete={setRegion} onRegionChangeComplete={setRegion}
showsUserLocation={true} showsUserLocation={true}
showsMyLocationButton={true} showsMyLocationButton={true}
> >
<UrlTile
urlTemplate="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
maximumZ={19}
flipY={false}
tileSize={256}
/>
{pharmacies.map((pharmacy) => ( {pharmacies.map((pharmacy) => (
<Marker <Marker
key={pharmacy.id} key={pharmacy.id}
+6 -5
View File
@@ -174,9 +174,11 @@ export default function SearchScreen() {
data={results} data={results}
keyExtractor={(item) => item.nregistro} keyExtractor={(item) => item.nregistro}
renderItem={({ item }) => <MedicineCard medicine={item} />} renderItem={({ item }) => <MedicineCard medicine={item} />}
ListHeaderComponent={ ListEmptyComponent={
<> products.length === 0 ? null : undefined
{products.length > 0 && ( }
ListFooterComponent={
products.length > 0 ? (
<View style={styles.section}> <View style={styles.section}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>{t('search.parapharmacy')}</Text> <Text style={[styles.sectionTitle, { color: colors.text }]}>{t('search.parapharmacy')}</Text>
{products.map((product) => ( {products.map((product) => (
@@ -198,8 +200,7 @@ export default function SearchScreen() {
</TouchableOpacity> </TouchableOpacity>
))} ))}
</View> </View>
)} ) : null
</>
} }
contentContainerStyle={[styles.list, isTablet && styles.listTablet]} contentContainerStyle={[styles.list, isTablet && styles.listTablet]}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
-4
View File
@@ -9,10 +9,6 @@ import { useAuthStore } from '../store/authStore';
import { registerForPushNotifications, addNotificationListener, addNotificationResponseListener } from '../services/notifications'; import { registerForPushNotifications, addNotificationListener, addNotificationResponseListener } from '../services/notifications';
import { ThemeProvider, useThemeContext } from '../components/ThemeProvider'; import { ThemeProvider, useThemeContext } from '../components/ThemeProvider';
import { LanguageProvider, useTranslation } from '../src/i18n'; import { LanguageProvider, useTranslation } from '../src/i18n';
import { initFaro } from '../services/faro';
// Boot Faro RUM once, as early as possible.
initFaro();
const queryClient = new QueryClient(); const queryClient = new QueryClient();
+35 -11
View File
@@ -3,6 +3,7 @@ import { View, Text, ScrollView, StyleSheet, TouchableOpacity, Linking } from 'r
import { useLocalSearchParams, useRouter } from 'expo-router'; import { useLocalSearchParams, useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import * as Location from 'expo-location'; import * as Location from 'expo-location';
import MapView, { Marker, UrlTile } from 'react-native-maps';
import { getMedicine, getMedicinePharmacies } from '../../services/medicines'; import { getMedicine, getMedicinePharmacies } from '../../services/medicines';
import { subscribeToMedicine, unsubscribeFromMedicine } from '../../services/notifications'; import { subscribeToMedicine, unsubscribeFromMedicine } from '../../services/notifications';
import { useAuth } from '../../hooks/useAuth'; import { useAuth } from '../../hooks/useAuth';
@@ -205,11 +206,38 @@ export default function MedicineDetailScreen() {
</View> </View>
{locatedPharmacies.length > 0 && ( {locatedPharmacies.length > 0 && (
<View style={[styles.mapContainer, { backgroundColor: colors.surfaceVariant }]}> <View style={styles.mapContainer}>
<Ionicons name="map-outline" size={48} color={colors.textSecondary} /> <MapView
<Text style={[styles.mapPlaceholder, { color: colors.textSecondary }]}> style={styles.map}
{t('medicine.mapSoon')} mapType="none"
</Text> initialRegion={{
latitude: mapCenter.latitude,
longitude: mapCenter.longitude,
latitudeDelta: 0.05,
longitudeDelta: 0.05,
}}
>
<UrlTile
urlTemplate="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
maximumZ={19}
flipY={false}
tileSize={256}
/>
{locatedPharmacies.map((pharm) => {
const lat = getPharmacyLat(pharm);
const lon = getPharmacyLon(pharm);
if (lat == null || lon == null) return null;
return (
<Marker
key={pharm.id}
coordinate={{ latitude: lat, longitude: lon }}
title={pharm.pharmacy?.name || pharm.name}
description={pharm.pharmacy?.address || pharm.address}
onCalloutPress={() => router.push(`/pharmacy/${pharm.pharmacy_id || pharm.id}`)}
/>
);
})}
</MapView>
</View> </View>
)} )}
@@ -362,13 +390,9 @@ const styles = StyleSheet.create({
height: 200, height: 200,
borderRadius: borderRadius.lg, borderRadius: borderRadius.lg,
overflow: 'hidden', overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',
gap: spacing.sm,
}, },
mapPlaceholder: { map: {
fontSize: 14, flex: 1,
fontWeight: '500',
}, },
pharmaciesSection: { pharmaciesSection: {
marginTop: spacing.sm, marginTop: spacing.sm,
+8 -1
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { View, Text, ScrollView, StyleSheet, TouchableOpacity, Linking } from 'react-native'; import { View, Text, ScrollView, StyleSheet, TouchableOpacity, Linking } from 'react-native';
import { useLocalSearchParams, useRouter } from 'expo-router'; import { useLocalSearchParams, useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import MapView, { Marker } from 'react-native-maps'; import MapView, { Marker, UrlTile } from 'react-native-maps';
import { getPharmacy, getPharmacyMedicines } from '../../services/pharmacies'; import { getPharmacy, getPharmacyMedicines } from '../../services/pharmacies';
import { subscribeToMedicine, unsubscribeFromMedicine } from '../../services/notifications'; import { subscribeToMedicine, unsubscribeFromMedicine } from '../../services/notifications';
import { useAuth } from '../../hooks/useAuth'; import { useAuth } from '../../hooks/useAuth';
@@ -125,6 +125,7 @@ export default function PharmacyDetailScreen() {
<View style={styles.mapContainer}> <View style={styles.mapContainer}>
<MapView <MapView
style={styles.map} style={styles.map}
mapType="none"
initialRegion={{ initialRegion={{
latitude: pharmacy.latitude, latitude: pharmacy.latitude,
longitude: pharmacy.longitude, longitude: pharmacy.longitude,
@@ -133,6 +134,12 @@ export default function PharmacyDetailScreen() {
}} }}
scrollEnabled={false} scrollEnabled={false}
> >
<UrlTile
urlTemplate="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
maximumZ={19}
flipY={false}
tileSize={256}
/>
<Marker <Marker
coordinate={{ coordinate={{
latitude: pharmacy.latitude, latitude: pharmacy.latitude,
-1
View File
@@ -4,7 +4,6 @@
"main": "expo-router/entry", "main": "expo-router/entry",
"dependencies": { "dependencies": {
"@expo/vector-icons": "^15.0.2", "@expo/vector-icons": "^15.0.2",
"@grafana/faro-react-native": "^1.3.0",
"@react-native-async-storage/async-storage": "2.2.0", "@react-native-async-storage/async-storage": "2.2.0",
"@tanstack/react-query": "^5.101.2", "@tanstack/react-query": "^5.101.2",
"axios": "^1.18.1", "axios": "^1.18.1",
-36
View File
@@ -1,36 +0,0 @@
import { initializeFaro } from '@grafana/faro-react-native';
let initialized = false;
/**
* Initializes Grafana Faro RUM for the React Native app.
*
* Telemetry (crashes, JS errors, console, app-start, memory/ANR vitals,
* session + screen tracking) is sent OTLP/HTTP to the Alloy collector.
*
* NOTE: `@grafana/faro-react-native` ships a native module, so it requires a
* custom dev build / EAS build (not Expo Go). Initialization is wrapped in a
* try/catch so a collector outage never crashes the app.
*/
export function initFaro(): void {
if (initialized) return;
initialized = true;
try {
const url =
process.env.EXPO_PUBLIC_FARO_URL || 'http://grafana.hacecalor.net:4318';
initializeFaro({
app: {
name: 'farmafinder-mobile',
version: '1.0.0',
environment: __DEV__ ? 'development' : 'production',
},
url,
sessionTracking: { enabled: true },
// Crash/JS error/console capture are on by default.
});
} catch (err) {
console.warn('[faro] initialization failed:', err);
}
}
-161
View File
@@ -138,7 +138,6 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@expo/vector-icons": "^15.0.2", "@expo/vector-icons": "^15.0.2",
"@grafana/faro-react-native": "^1.3.0",
"@react-native-async-storage/async-storage": "2.2.0", "@react-native-async-storage/async-storage": "2.2.0",
"@tanstack/react-query": "^5.101.2", "@tanstack/react-query": "^5.101.2",
"axios": "^1.18.1", "axios": "^1.18.1",
@@ -403,158 +402,6 @@
"react-native": "*" "react-native": "*"
} }
}, },
"apps/frontend-mobile/node_modules/@grafana/faro-core": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/@grafana/faro-core/-/faro-core-2.8.2.tgz",
"integrity": "sha512-63C6+N/P9/ySUMaGut8yVb1DkPuHS5I/lfRm97+aDXFj3+8pxRT/QzyXNe0r6KMU5O95wC9FLVGxG9ZvkwEhJQ==",
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/otlp-transformer": "^0.219.0"
}
},
"apps/frontend-mobile/node_modules/@grafana/faro-react-native": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@grafana/faro-react-native/-/faro-react-native-1.3.0.tgz",
"integrity": "sha512-KeohD3S2xmiukL5YW3mOGDqvFiwpD9IzJ2EtZ50ktEsnA+6EMt2AtJ8RzJfAamGTYqemBpDiw51rqB4ID+C2xg==",
"license": "Apache-2.0",
"dependencies": {
"@grafana/faro-core": "^2.7.0",
"@react-native-async-storage/async-storage": "^1.21.0",
"react-native-device-info": "^11.1.0"
},
"peerDependencies": {
"@react-navigation/native": ">=6.0.0",
"react": ">=18.0.0",
"react-native": ">=0.70.0",
"react-native-mmkv": ">=2.0.0"
},
"peerDependenciesMeta": {
"@react-navigation/native": {
"optional": true
},
"react-native-mmkv": {
"optional": true
}
}
},
"apps/frontend-mobile/node_modules/@grafana/faro-react-native/node_modules/@react-native-async-storage/async-storage": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.24.0.tgz",
"integrity": "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g==",
"dependencies": {
"merge-options": "^3.0.4"
},
"peerDependencies": {
"react-native": "^0.0.0-0 || >=0.60 <1.0"
}
},
"apps/frontend-mobile/node_modules/@opentelemetry/api-logs": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz",
"integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==",
"dependencies": {
"@opentelemetry/api": "^1.3.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"apps/frontend-mobile/node_modules/@opentelemetry/core": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz",
"integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"apps/frontend-mobile/node_modules/@opentelemetry/otlp-transformer": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.219.0.tgz",
"integrity": "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ==",
"dependencies": {
"@opentelemetry/api-logs": "0.219.0",
"@opentelemetry/core": "2.8.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/sdk-logs": "0.219.0",
"@opentelemetry/sdk-metrics": "2.8.0",
"@opentelemetry/sdk-trace-base": "2.8.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"apps/frontend-mobile/node_modules/@opentelemetry/resources": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz",
"integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==",
"dependencies": {
"@opentelemetry/core": "2.8.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"apps/frontend-mobile/node_modules/@opentelemetry/sdk-logs": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.219.0.tgz",
"integrity": "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==",
"dependencies": {
"@opentelemetry/api-logs": "0.219.0",
"@opentelemetry/core": "2.8.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.4.0 <1.10.0"
}
},
"apps/frontend-mobile/node_modules/@opentelemetry/sdk-metrics": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.8.0.tgz",
"integrity": "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==",
"dependencies": {
"@opentelemetry/core": "2.8.0",
"@opentelemetry/resources": "2.8.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.9.0 <1.10.0"
}
},
"apps/frontend-mobile/node_modules/@opentelemetry/sdk-trace-base": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz",
"integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==",
"dependencies": {
"@opentelemetry/core": "2.8.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"apps/frontend-mobile/node_modules/@react-native-async-storage/async-storage": { "apps/frontend-mobile/node_modules/@react-native-async-storage/async-storage": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
@@ -1277,14 +1124,6 @@
} }
} }
}, },
"apps/frontend-mobile/node_modules/react-native-device-info": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-11.1.0.tgz",
"integrity": "sha512-hzXJSObJdezEz0hF7MAJ3tGeoesuQWenXXt9mrQR9Mjb8kXpZ09rqSsZ/quNpJdZpQ3rYiFa3/0GFG5KNn9PBg==",
"peerDependencies": {
"react-native": "*"
}
},
"apps/frontend-mobile/node_modules/react-native-drawer-layout": { "apps/frontend-mobile/node_modules/react-native-drawer-layout": {
"version": "4.2.7", "version": "4.2.7",
"resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-4.2.7.tgz", "resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-4.2.7.tgz",