diff --git a/apps/frontend-mobile/.env.example b/apps/frontend-mobile/.env.example
index 0668dde..b544ae9 100644
--- a/apps/frontend-mobile/.env.example
+++ b/apps/frontend-mobile/.env.example
@@ -6,8 +6,3 @@ EXPO_PUBLIC_API_URL=http://localhost:3001/api
# For production builds, update this to:
# 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
diff --git a/apps/frontend-mobile/app.json b/apps/frontend-mobile/app.json
index 55b0a94..8fe6891 100644
--- a/apps/frontend-mobile/app.json
+++ b/apps/frontend-mobile/app.json
@@ -24,13 +24,7 @@
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#007AFF"
},
- "package": "com.farmafinder.app",
- "googleServicesFile": "./google-services.json",
- "config": {
- "googleMaps": {
- "apiKey": ""
- }
- }
+ "package": "com.farmafinder.app"
},
"plugins": [
"expo-router",
diff --git a/apps/frontend-mobile/app/(tabs)/map.tsx b/apps/frontend-mobile/app/(tabs)/map.tsx
index 6e0d523..acb25e2 100644
--- a/apps/frontend-mobile/app/(tabs)/map.tsx
+++ b/apps/frontend-mobile/app/(tabs)/map.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
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 { getPharmacies } from '../../services/pharmacies';
import { LoadingSpinner } from '../../components/LoadingSpinner';
@@ -54,11 +54,18 @@ export default function MapScreen() {
+
{pharmacies.map((pharmacy) => (
item.nregistro}
renderItem={({ item }) => }
- ListHeaderComponent={
- <>
- {products.length > 0 && (
-
- {t('search.parapharmacy')}
- {products.map((product) => (
- router.push(`/product/${product.source}/${product.id || product._id}`)}
- activeOpacity={0.7}
- >
-
-
-
- {t('search.parapharmacy')}
-
+ ListEmptyComponent={
+ products.length === 0 ? null : undefined
+ }
+ ListFooterComponent={
+ products.length > 0 ? (
+
+ {t('search.parapharmacy')}
+ {products.map((product) => (
+ router.push(`/product/${product.source}/${product.id || product._id}`)}
+ activeOpacity={0.7}
+ >
+
+
+
+ {t('search.parapharmacy')}
- {product.name}
- {product.brand} • {product.price}€
-
- ))}
-
- )}
- >
+ {product.name}
+ {product.brand} • {product.price}€
+
+
+ ))}
+
+ ) : null
}
contentContainerStyle={[styles.list, isTablet && styles.listTablet]}
showsVerticalScrollIndicator={false}
diff --git a/apps/frontend-mobile/app/_layout.tsx b/apps/frontend-mobile/app/_layout.tsx
index 3283cf9..45fa389 100644
--- a/apps/frontend-mobile/app/_layout.tsx
+++ b/apps/frontend-mobile/app/_layout.tsx
@@ -9,10 +9,6 @@ import { useAuthStore } from '../store/authStore';
import { registerForPushNotifications, addNotificationListener, addNotificationResponseListener } from '../services/notifications';
import { ThemeProvider, useThemeContext } from '../components/ThemeProvider';
import { LanguageProvider, useTranslation } from '../src/i18n';
-import { initFaro } from '../services/faro';
-
-// Boot Faro RUM once, as early as possible.
-initFaro();
const queryClient = new QueryClient();
diff --git a/apps/frontend-mobile/app/medicine/[id].tsx b/apps/frontend-mobile/app/medicine/[id].tsx
index 1914c45..20f053a 100644
--- a/apps/frontend-mobile/app/medicine/[id].tsx
+++ b/apps/frontend-mobile/app/medicine/[id].tsx
@@ -3,6 +3,7 @@ import { View, Text, ScrollView, StyleSheet, TouchableOpacity, Linking } from 'r
import { useLocalSearchParams, useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import * as Location from 'expo-location';
+import MapView, { Marker, UrlTile } from 'react-native-maps';
import { getMedicine, getMedicinePharmacies } from '../../services/medicines';
import { subscribeToMedicine, unsubscribeFromMedicine } from '../../services/notifications';
import { useAuth } from '../../hooks/useAuth';
@@ -205,11 +206,38 @@ export default function MedicineDetailScreen() {
{locatedPharmacies.length > 0 && (
-
-
-
- {t('medicine.mapSoon')}
-
+
+
+
+ {locatedPharmacies.map((pharm) => {
+ const lat = getPharmacyLat(pharm);
+ const lon = getPharmacyLon(pharm);
+ if (lat == null || lon == null) return null;
+ return (
+ router.push(`/pharmacy/${pharm.pharmacy_id || pharm.id}`)}
+ />
+ );
+ })}
+
)}
@@ -362,13 +390,9 @@ const styles = StyleSheet.create({
height: 200,
borderRadius: borderRadius.lg,
overflow: 'hidden',
- alignItems: 'center',
- justifyContent: 'center',
- gap: spacing.sm,
},
- mapPlaceholder: {
- fontSize: 14,
- fontWeight: '500',
+ map: {
+ flex: 1,
},
pharmaciesSection: {
marginTop: spacing.sm,
diff --git a/apps/frontend-mobile/app/pharmacy/[id].tsx b/apps/frontend-mobile/app/pharmacy/[id].tsx
index a2165a0..721e033 100644
--- a/apps/frontend-mobile/app/pharmacy/[id].tsx
+++ b/apps/frontend-mobile/app/pharmacy/[id].tsx
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { View, Text, ScrollView, StyleSheet, TouchableOpacity, Linking } from 'react-native';
import { useLocalSearchParams, useRouter } from 'expo-router';
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 { subscribeToMedicine, unsubscribeFromMedicine } from '../../services/notifications';
import { useAuth } from '../../hooks/useAuth';
@@ -125,6 +125,7 @@ export default function PharmacyDetailScreen() {
+
=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": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
@@ -1278,14 +1125,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": {
"version": "4.2.7",
"resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-4.2.7.tgz",