diff --git a/docs/superpowers/plans/2026-07-06-farmafinder-mobile-implementation.md b/docs/superpowers/plans/2026-07-06-farmafinder-mobile-implementation.md
new file mode 100644
index 0000000..4f01b25
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-06-farmafinder-mobile-implementation.md
@@ -0,0 +1,3417 @@
+# FarmaFinder Mobile Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Create a React Native mobile frontend using Expo that provides native iOS/Android experience for FarmaFinder, with core features (medicine search, pharmacy map) plus native extras (push notifications, barcode scanner, biometrics).
+
+**Architecture:** Expo Router for file-based routing, Zustand for state management, TanStack Query for API caching, react-native-maps for pharmacy locations. Separate `frontend-mobile/` directory sharing only the backend API.
+
+**Tech Stack:** Expo SDK 52+, React Native 0.76+, Expo Router v4, Zustand, TanStack Query, Axios, react-native-maps, expo-camera, expo-notifications
+
+---
+
+## File Structure Overview
+
+```
+FarmaFinder/
+├── frontend-mobile/ ← NEW
+│ ├── app/
+│ │ ├── _layout.tsx ← Root layout (providers)
+│ │ ├── (tabs)/
+│ │ │ ├── _layout.tsx ← Tab navigator
+│ │ │ ├── index.tsx ← Home (search)
+│ │ │ ├── map.tsx ← Map view
+│ │ │ └── profile.tsx ← User profile
+│ │ ├── medicine/[id].tsx ← Medicine detail
+│ │ ├── pharmacy/[id].tsx ← Pharmacy detail
+│ │ └── auth/
+│ │ ├── login.tsx ← Login screen
+│ │ └── register.tsx ← Register screen
+│ ├── components/
+│ │ ├── SearchBar.tsx ← Search input with debounce
+│ │ ├── MedicineCard.tsx ← Medicine result card
+│ │ ├── PharmacyMarker.tsx ← Custom map marker
+│ │ ├── StockBadge.tsx ← Stock availability badge
+│ │ └── LoadingSpinner.tsx ← Loading indicator
+│ ├── services/
+│ │ ├── api.ts ← Axios client
+│ │ ├── medicines.ts ← Medicine API calls
+│ │ ├── pharmacies.ts ← Pharmacy API calls
+│ │ └── auth.ts ← Auth API calls
+│ ├── store/
+│ │ ├── authStore.ts ← Auth state (Zustand)
+│ │ └── searchStore.ts ← Search state (Zustand)
+│ ├── hooks/
+│ │ ├── useDebounce.ts ← Debounce hook
+│ │ └── useAuth.ts ← Auth hook
+│ ├── constants/
+│ │ ├── theme.ts ← Colors, spacing
+│ │ └── config.ts ← API URLs, settings
+│ ├── types/
+│ │ └── index.ts ← TypeScript types
+│ ├── assets/
+│ │ └── icon.png ← App icon
+│ ├── app.json ← Expo config
+│ ├── eas.json ← EAS Build config
+│ └── package.json ← Dependencies
+├── frontend/ ← EXISTING (no changes)
+├── backend/ ← EXISTING (no changes)
+└── package.json ← MODIFY (add mobile scripts)
+```
+
+---
+
+## Task 1: Initialize Expo Project
+
+**Files:**
+- Create: `frontend-mobile/` (entire directory structure)
+- Create: `frontend-mobile/package.json`
+- Create: `frontend-mobile/app.json`
+- Create: `frontend-mobile/tsconfig.json`
+- Create: `frontend-mobile/babel.config.js`
+
+- [ ] **Step 1: Create project directory and initialize**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+mkdir -p frontend-mobile
+cd frontend-mobile
+npx create-expo-app@latest . --template blank-typescript
+```
+
+- [ ] **Step 2: Install core dependencies**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder/frontend-mobile
+npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar
+npx expo install react-native-gesture-handler react-native-reanimated
+```
+
+- [ ] **Step 3: Install additional dependencies**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder/frontend-mobile
+npm install zustand @tanstack/react-query axios
+npm install react-native-maps
+npx expo install expo-camera expo-notifications expo-local-authentication expo-secure-store @react-native-async-storage/async-storage
+```
+
+- [ ] **Step 4: Configure app.json**
+
+```json
+{
+ "expo": {
+ "name": "FarmaFinder",
+ "slug": "farmafinder",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "userInterfaceStyle": "light",
+ "newArchEnabled": true,
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#007AFF"
+ },
+ "ios": {
+ "supportsTablet": true,
+ "bundleIdentifier": "com.farmafinder.app"
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#007AFF"
+ },
+ "package": "com.farmafinder.app"
+ },
+ "plugins": [
+ "expo-router",
+ [
+ "expo-camera",
+ {
+ "cameraPermission": "Allow FarmaFinder to access your camera for scanning barcodes"
+ }
+ ],
+ [
+ "expo-notifications",
+ {
+ "icon": "./assets/notification-icon.png",
+ "color": "#007AFF"
+ }
+ ]
+ ],
+ "scheme": "farmafinder"
+ }
+}
+```
+
+- [ ] **Step 5: Configure babel.config.js**
+
+```javascript
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ plugins: ['react-native-reanimated/plugin'],
+ };
+};
+```
+
+- [ ] **Step 6: Configure tsconfig.json**
+
+```json
+{
+ "extends": "expo/tsconfig.base",
+ "compilerOptions": {
+ "strict": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
+}
+```
+
+- [ ] **Step 7: Verify project compiles**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder/frontend-mobile
+npx expo start --clear
+```
+
+Expected: Expo dev server starts without errors
+
+- [ ] **Step 8: Commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add frontend-mobile/
+git commit -m "feat: initialize Expo project for mobile frontend"
+```
+
+---
+
+## Task 2: Setup Navigation Structure
+
+**Files:**
+- Create: `frontend-mobile/app/_layout.tsx`
+- Create: `frontend-mobile/app/(tabs)/_layout.tsx`
+- Create: `frontend-mobile/app/(tabs)/index.tsx`
+- Create: `frontend-mobile/app/(tabs)/map.tsx`
+- Create: `frontend-mobile/app/(tabs)/profile.tsx`
+
+- [ ] **Step 1: Create root layout**
+
+```typescript
+// frontend-mobile/app/_layout.tsx
+import { Stack } from 'expo-router';
+import { StatusBar } from 'expo-status-bar';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { GestureHandlerRootView } from 'react-native-gesture-handler';
+
+const queryClient = new QueryClient();
+
+export default function RootLayout() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+- [ ] **Step 2: Create tab navigator**
+
+```typescript
+// frontend-mobile/app/(tabs)/_layout.tsx
+import { Tabs } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+
+export default function TabLayout() {
+ return (
+
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+
+ );
+}
+```
+
+- [ ] **Step 3: Create Home screen (placeholder)**
+
+```typescript
+// frontend-mobile/app/(tabs)/index.tsx
+import { View, Text, StyleSheet } from 'react-native';
+
+export default function HomeScreen() {
+ return (
+
+ FarmaFinder
+ Busca medicamentos
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: '#F2F2F7',
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: 'bold',
+ color: '#1C1C1E',
+ },
+ subtitle: {
+ fontSize: 16,
+ color: '#8E8E93',
+ marginTop: 8,
+ },
+});
+```
+
+- [ ] **Step 4: Create Map screen (placeholder)**
+
+```typescript
+// frontend-mobile/app/(tabs)/map.tsx
+import { View, Text, StyleSheet } from 'react-native';
+
+export default function MapScreen() {
+ return (
+
+ Mapa de Farmacias
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: '#F2F2F7',
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: '#1C1C1E',
+ },
+});
+```
+
+- [ ] **Step 5: Create Profile screen (placeholder)**
+
+```typescript
+// frontend-mobile/app/(tabs)/profile.tsx
+import { View, Text, StyleSheet } from 'react-native';
+
+export default function ProfileScreen() {
+ return (
+
+ Perfil
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: '#F2F2F7',
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: '#1C1C1E',
+ },
+});
+```
+
+- [ ] **Step 6: Update package.json with scripts**
+
+```json
+{
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web"
+ }
+}
+```
+
+- [ ] **Step 7: Verify navigation works**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder/frontend-mobile
+npx expo start
+```
+
+Expected: App shows with 3 tabs (Buscar, Mapa, Perfil)
+
+- [ ] **Step 8: Commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add frontend-mobile/
+git commit -m "feat: setup navigation structure with Expo Router"
+```
+
+---
+
+## Task 3: Create TypeScript Types
+
+**Files:**
+- Create: `frontend-mobile/types/index.ts`
+
+- [ ] **Step 1: Create types file**
+
+```typescript
+// frontend-mobile/types/index.ts
+export interface Medicine {
+ nregistro: string;
+ nombre: string;
+ principioActivo: string;
+ laboratorio: string;
+ formaFarmaceutica: string;
+ precio: number | null;
+ stock: number;
+ disponibilidad: 'disponible' | 'sin_stock' | 'bajo_stock';
+}
+
+export interface Pharmacy {
+ id: number;
+ name: string;
+ address: string;
+ phone: string;
+ latitude: number;
+ longitude: number;
+}
+
+export interface PharmacyMedicine {
+ id: number;
+ pharmacy_id: number;
+ medicine_nregistro: string;
+ medicine_name: string;
+ price: number;
+ stock: number;
+ pharmacy?: Pharmacy;
+}
+
+export interface User {
+ id: number;
+ username: string;
+ is_admin: boolean;
+}
+
+export interface AuthResponse {
+ user: User;
+ token: string;
+}
+
+export interface SearchResponse {
+ medicines: Medicine[];
+ total: number;
+}
+
+export interface ApiResponse {
+ data: T;
+ success: boolean;
+ error?: string;
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add frontend-mobile/types/
+git commit -m "feat: add TypeScript types for mobile app"
+```
+
+---
+
+## Task 4: Create API Client and Services
+
+**Files:**
+- Create: `frontend-mobile/constants/config.ts`
+- Create: `frontend-mobile/services/api.ts`
+- Create: `frontend-mobile/services/medicines.ts`
+- Create: `frontend-mobile/services/pharmacies.ts`
+- Create: `frontend-mobile/services/auth.ts`
+
+- [ ] **Step 1: Create config constants**
+
+```typescript
+// frontend-mobile/constants/config.ts
+import Constants from 'expo-constants';
+
+const ENV = {
+ development: {
+ API_BASE_URL: 'http://localhost:3001/api',
+ },
+ production: {
+ API_BASE_URL: 'https://your-production-api.com/api',
+ },
+};
+
+const environment = Constants.expoConfig?.extra?.environment || (__DEV__ ? 'development' : 'production');
+
+export const config = {
+ API_BASE_URL: ENV[environment as keyof typeof ENV]?.API_BASE_URL || ENV.development.API_BASE_URL,
+ SEARCH_DEBOUNCE_MS: 300,
+ CACHE_STALE_TIME: 5 * 60 * 1000, // 5 minutes
+ CACHE_CACHE_TIME: 30 * 60 * 1000, // 30 minutes
+};
+```
+
+- [ ] **Step 2: Create API client**
+
+```typescript
+// frontend-mobile/services/api.ts
+import axios from 'axios';
+import * as SecureStore from 'expo-secure-store';
+import { config } from '../constants/config';
+
+export const api = axios.create({
+ baseURL: config.API_BASE_URL,
+ timeout: 10000,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+// Request interceptor - add auth token
+api.interceptors.request.use(
+ async (axiosConfig) => {
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (token) {
+ axiosConfig.headers.Authorization = `Bearer ${token}`;
+ }
+ return axiosConfig;
+ },
+ (error) => Promise.reject(error)
+);
+
+// Response interceptor - handle errors
+api.interceptors.response.use(
+ (response) => response,
+ async (error) => {
+ if (error.response?.status === 401) {
+ await SecureStore.deleteItemAsync('auth_token');
+ }
+ return Promise.reject(error);
+ }
+);
+
+export default api;
+```
+
+- [ ] **Step 3: Create medicines service**
+
+```typescript
+// frontend-mobile/services/medicines.ts
+import api from './api';
+import { Medicine, PharmacyMedicine } from '../types';
+
+export async function searchMedicines(query: string): Promise {
+ const response = await api.get(`/medicines/search?q=${encodeURIComponent(query)}`);
+ return response.data;
+}
+
+export async function getMedicine(nregistro: string): Promise {
+ const response = await api.get(`/medicines/${nregistro}`);
+ return response.data;
+}
+
+export async function getMedicinePharmacies(nregistro: string): Promise {
+ const response = await api.get(`/medicines/${nregistro}/pharmacies`);
+ return response.data;
+}
+```
+
+- [ ] **Step 4: Create pharmacies service**
+
+```typescript
+// frontend-mobile/services/pharmacies.ts
+import api from './api';
+import { Pharmacy } from '../types';
+
+export async function getPharmacies(): Promise {
+ const response = await api.get('/pharmacies');
+ return response.data;
+}
+
+export async function getPharmacy(id: number): Promise {
+ const response = await api.get(`/pharmacies/${id}`);
+ return response.data;
+}
+
+export async function getPharmacyMedicines(id: number): Promise {
+ const response = await api.get(`/pharmacies/${id}/medicines`);
+ return response.data;
+}
+```
+
+- [ ] **Step 5: Create auth service**
+
+```typescript
+// frontend-mobile/services/auth.ts
+import api from './api';
+import * as SecureStore from 'expo-secure-store';
+import { AuthResponse, User } from '../types';
+
+export async function login(username: string, password: string): Promise {
+ const response = await api.post('/auth/login', { username, password });
+ const { user, token } = response.data;
+
+ await SecureStore.setItemAsync('auth_token', token);
+ await SecureStore.setItemAsync('user', JSON.stringify(user));
+
+ return response.data;
+}
+
+export async function logout(): Promise {
+ await api.post('/auth/logout');
+ await SecureStore.deleteItemAsync('auth_token');
+ await SecureStore.deleteItemAsync('user');
+}
+
+export async function checkAuth(): Promise {
+ try {
+ const response = await api.get('/auth/check');
+ return response.data.user;
+ } catch {
+ return null;
+ }
+}
+
+export async function getStoredUser(): Promise {
+ const userStr = await SecureStore.getItemAsync('user');
+ return userStr ? JSON.parse(userStr) : null;
+}
+
+export async function register(username: string, password: string): Promise {
+ const response = await api.post('/auth/register', { username, password });
+ const { user, token } = response.data;
+
+ await SecureStore.setItemAsync('auth_token', token);
+ await SecureStore.setItemAsync('user', JSON.stringify(user));
+
+ return response.data;
+}
+```
+
+- [ ] **Step 6: Commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add frontend-mobile/
+git commit -m "feat: add API client and services for medicines, pharmacies, auth"
+```
+
+---
+
+## Task 5: Create State Management (Zustand)
+
+**Files:**
+- Create: `frontend-mobile/store/authStore.ts`
+- Create: `frontend-mobile/store/searchStore.ts`
+- Create: `frontend-mobile/hooks/useAuth.ts`
+
+- [ ] **Step 1: Create auth store**
+
+```typescript
+// frontend-mobile/store/authStore.ts
+import { create } from 'zustand';
+import { User } from '../types';
+import * as authService from '../services/auth';
+
+interface AuthState {
+ user: User | null;
+ isLoading: boolean;
+ isAuthenticated: boolean;
+ login: (username: string, password: string) => Promise;
+ logout: () => Promise;
+ checkAuth: () => Promise;
+}
+
+export const useAuthStore = create((set) => ({
+ user: null,
+ isLoading: true,
+ isAuthenticated: false,
+
+ login: async (username: string, password: string) => {
+ const { user } = await authService.login(username, password);
+ set({ user, isAuthenticated: true });
+ },
+
+ logout: async () => {
+ await authService.logout();
+ set({ user: null, isAuthenticated: false });
+ },
+
+ checkAuth: async () => {
+ set({ isLoading: true });
+ const user = await authService.checkAuth() || await authService.getStoredUser();
+ set({
+ user,
+ isAuthenticated: !!user,
+ isLoading: false
+ });
+ },
+}));
+```
+
+- [ ] **Step 2: Create search store**
+
+```typescript
+// frontend-mobile/store/searchStore.ts
+import { create } from 'zustand';
+import { Medicine } from '../types';
+
+interface SearchState {
+ query: string;
+ results: Medicine[];
+ isLoading: boolean;
+ error: string | null;
+ setQuery: (query: string) => void;
+ setResults: (results: Medicine[]) => void;
+ setLoading: (loading: boolean) => void;
+ setError: (error: string | null) => void;
+ clearResults: () => void;
+}
+
+export const useSearchStore = create((set) => ({
+ query: '',
+ results: [],
+ isLoading: false,
+ error: null,
+
+ setQuery: (query) => set({ query }),
+ setResults: (results) => set({ results, isLoading: false }),
+ setLoading: (isLoading) => set({ isLoading }),
+ setError: (error) => set({ error, isLoading: false }),
+ clearResults: () => set({ results: [], query: '', error: null }),
+}));
+```
+
+- [ ] **Step 3: Create useAuth hook**
+
+```typescript
+// frontend-mobile/hooks/useAuth.ts
+import { useEffect } from 'react';
+import { useAuthStore } from '../store/authStore';
+
+export function useAuth() {
+ const { user, isLoading, isAuthenticated, login, logout, checkAuth } = useAuthStore();
+
+ useEffect(() => {
+ checkAuth();
+ }, []);
+
+ return {
+ user,
+ isLoading,
+ isAuthenticated,
+ login,
+ logout,
+ isAdmin: user?.is_admin ?? false,
+ };
+}
+```
+
+- [ ] **Step 4: Create useDebounce hook**
+
+```typescript
+// frontend-mobile/hooks/useDebounce.ts
+import { useState, useEffect } from 'react';
+
+export function useDebounce(value: T, delay: number): T {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setDebouncedValue(value);
+ }, delay);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }, [value, delay]);
+
+ return debouncedValue;
+}
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add frontend-mobile/
+git commit -m "feat: add Zustand stores and custom hooks"
+```
+
+---
+
+## Task 6: Create Constants and Theme
+
+**Files:**
+- Create: `frontend-mobile/constants/theme.ts`
+
+- [ ] **Step 1: Create theme constants**
+
+```typescript
+// frontend-mobile/constants/theme.ts
+export const colors = {
+ // Primary
+ primary: '#007AFF',
+ primaryLight: '#4DA3FF',
+ primaryDark: '#0056CC',
+
+ // Semantic
+ success: '#34C759',
+ danger: '#FF3B30',
+ warning: '#FF9500',
+
+ // Neutral
+ background: '#F2F2F7',
+ card: '#FFFFFF',
+ border: '#E5E5EA',
+ separator: '#C6C6C8',
+
+ // Text
+ text: '#1C1C1E',
+ textSecondary: '#8E8E93',
+ textInverse: '#FFFFFF',
+ textLink: '#007AFF',
+};
+
+export const spacing = {
+ xs: 4,
+ sm: 8,
+ md: 16,
+ lg: 24,
+ xl: 32,
+ xxl: 48,
+};
+
+export const borderRadius = {
+ sm: 8,
+ md: 12,
+ lg: 16,
+ xl: 24,
+};
+
+export const typography = {
+ title: {
+ fontSize: 28,
+ fontWeight: 'bold' as const,
+ },
+ heading: {
+ fontSize: 20,
+ fontWeight: '600' as const,
+ },
+ body: {
+ fontSize: 16,
+ fontWeight: 'normal' as const,
+ },
+ caption: {
+ fontSize: 12,
+ fontWeight: 'normal' as const,
+ },
+};
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add frontend-mobile/constants/
+git commit -m "feat: add theme constants (colors, spacing, typography)"
+```
+
+---
+
+## Task 7: Create UI Components
+
+**Files:**
+- Create: `frontend-mobile/components/SearchBar.tsx`
+- Create: `frontend-mobile/components/MedicineCard.tsx`
+- Create: `frontend-mobile/components/StockBadge.tsx`
+- Create: `frontend-mobile/components/LoadingSpinner.tsx`
+
+- [ ] **Step 1: Create SearchBar component**
+
+```typescript
+// frontend-mobile/components/SearchBar.tsx
+import React, { useState } from 'react';
+import { View, TextInput, StyleSheet, TouchableOpacity } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { colors, spacing, borderRadius } from '../constants/theme';
+
+interface SearchBarProps {
+ placeholder?: string;
+ onSearch: (query: string) => void;
+ value?: string;
+ onChangeText?: (text: string) => void;
+}
+
+export function SearchBar({
+ placeholder = 'Buscar medicamentos...',
+ onSearch,
+ value,
+ onChangeText
+}: SearchBarProps) {
+ const [localValue, setLocalValue] = useState(value || '');
+
+ const handleChange = (text: string) => {
+ setLocalValue(text);
+ onChangeText?.(text);
+ };
+
+ const handleSubmit = () => {
+ onSearch(localValue);
+ };
+
+ const handleClear = () => {
+ setLocalValue('');
+ onChangeText?.('');
+ onSearch('');
+ };
+
+ return (
+
+
+
+ {localValue.length > 0 && (
+
+
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: colors.card,
+ borderRadius: borderRadius.md,
+ paddingHorizontal: spacing.md,
+ paddingVertical: spacing.sm,
+ marginHorizontal: spacing.md,
+ marginVertical: spacing.sm,
+ },
+ icon: {
+ marginRight: spacing.sm,
+ },
+ input: {
+ flex: 1,
+ fontSize: 16,
+ color: colors.text,
+ paddingVertical: spacing.xs,
+ },
+ clearButton: {
+ marginLeft: spacing.sm,
+ },
+});
+```
+
+- [ ] **Step 2: Create MedicineCard component**
+
+```typescript
+// frontend-mobile/components/MedicineCard.tsx
+import React from 'react';
+import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
+import { useRouter } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { colors, spacing, borderRadius } from '../constants/theme';
+import { StockBadge } from './StockBadge';
+import { Medicine } from '../types';
+
+interface MedicineCardProps {
+ medicine: Medicine;
+}
+
+export function MedicineCard({ medicine }: MedicineCardProps) {
+ const router = useRouter();
+
+ const handlePress = () => {
+ router.push(`/medicine/${medicine.nregistro}`);
+ };
+
+ return (
+
+
+
+ {medicine.nombre}
+
+
+
+
+
+ {medicine.principioActivo}
+
+
+
+
+
+
+ {medicine.precio ? `${medicine.precio.toFixed(2)} €` : 'Sin precio'}
+
+
+
+
+
+
+ {medicine.laboratorio}
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ card: {
+ backgroundColor: colors.card,
+ borderRadius: borderRadius.md,
+ padding: spacing.md,
+ marginHorizontal: spacing.md,
+ marginVertical: spacing.xs,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
+ marginBottom: spacing.xs,
+ },
+ name: {
+ flex: 1,
+ fontSize: 16,
+ fontWeight: '600',
+ color: colors.text,
+ marginRight: spacing.sm,
+ },
+ principioActivo: {
+ fontSize: 14,
+ color: colors.textSecondary,
+ marginBottom: spacing.sm,
+ },
+ footer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ priceContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ price: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: colors.text,
+ marginLeft: spacing.xs,
+ },
+ labContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'flex-end',
+ },
+ laboratorio: {
+ fontSize: 12,
+ color: colors.textSecondary,
+ marginLeft: spacing.xs,
+ maxWidth: 120,
+ },
+});
+```
+
+- [ ] **Step 3: Create StockBadge component**
+
+```typescript
+// frontend-mobile/components/StockBadge.tsx
+import React from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+import { colors, borderRadius, spacing } from '../constants/theme';
+
+interface StockBadgeProps {
+ stock: number;
+}
+
+export function StockBadge({ stock }: StockBadgeProps) {
+ const getBadgeStyle = () => {
+ if (stock === 0) return styles.danger;
+ if (stock < 5) return styles.warning;
+ return styles.success;
+ };
+
+ const getText = () => {
+ if (stock === 0) return 'Sin stock';
+ if (stock < 5) return `Bajo (${stock})`;
+ return `Disponible (${stock})`;
+ };
+
+ return (
+
+ {getText()}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ badge: {
+ paddingHorizontal: spacing.sm,
+ paddingVertical: spacing.xs,
+ borderRadius: borderRadius.sm,
+ },
+ success: {
+ backgroundColor: '#D4EDDA',
+ },
+ warning: {
+ backgroundColor: '#FFF3CD',
+ },
+ danger: {
+ backgroundColor: '#F8D7DA',
+ },
+ text: {
+ fontSize: 12,
+ fontWeight: '600',
+ },
+});
+```
+
+- [ ] **Step 4: Create LoadingSpinner component**
+
+```typescript
+// frontend-mobile/components/LoadingSpinner.tsx
+import React from 'react';
+import { View, ActivityIndicator, Text, StyleSheet } from 'react-native';
+import { colors, spacing } from '../constants/theme';
+
+interface LoadingSpinnerProps {
+ message?: string;
+}
+
+export function LoadingSpinner({ message = 'Cargando...' }: LoadingSpinnerProps) {
+ return (
+
+
+ {message}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: spacing.xl,
+ },
+ message: {
+ marginTop: spacing.md,
+ fontSize: 16,
+ color: colors.textSecondary,
+ },
+});
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add frontend-mobile/components/
+git commit -m "feat: add UI components (SearchBar, MedicineCard, StockBadge, LoadingSpinner)"
+```
+
+---
+
+## Task 8: Implement Home Screen (Search)
+
+**Files:**
+- Modify: `frontend-mobile/app/(tabs)/index.tsx`
+
+- [ ] **Step 1: Implement search functionality**
+
+```typescript
+// frontend-mobile/app/(tabs)/index.tsx
+import React, { useState, useEffect } from 'react';
+import { View, FlatList, StyleSheet, Text } from 'react-native';
+import { SearchBar } from '../../components/SearchBar';
+import { MedicineCard } from '../../components/MedicineCard';
+import { LoadingSpinner } from '../../components/LoadingSpinner';
+import { useDebounce } from '../../hooks/useDebounce';
+import { searchMedicines } from '../../services/medicines';
+import { colors, spacing } from '../../constants/theme';
+import { Medicine } from '../../types';
+import { config } from '../../constants/config';
+
+export default function HomeScreen() {
+ const [query, setQuery] = useState('');
+ const [results, setResults] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const debouncedQuery = useDebounce(query, config.SEARCH_DEBOUNCE_MS);
+
+ useEffect(() => {
+ if (debouncedQuery.length < 2) {
+ setResults([]);
+ return;
+ }
+
+ const fetchResults = async () => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ const data = await searchMedicines(debouncedQuery);
+ setResults(data);
+ } catch (err) {
+ setError('Error al buscar medicamentos');
+ console.error(err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchResults();
+ }, [debouncedQuery]);
+
+ const handleSearch = (searchQuery: string) => {
+ setQuery(searchQuery);
+ };
+
+ return (
+
+
+
+ {isLoading && }
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {!isLoading && !error && results.length === 0 && query.length >= 2 && (
+
+ No se encontraron medicamentos
+
+ )}
+
+ item.nregistro}
+ renderItem={({ item }) => }
+ contentContainerStyle={styles.list}
+ showsVerticalScrollIndicator={false}
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ list: {
+ paddingBottom: spacing.xl,
+ },
+ errorContainer: {
+ padding: spacing.md,
+ marginHorizontal: spacing.md,
+ backgroundColor: '#F8D7DA',
+ borderRadius: 8,
+ },
+ errorText: {
+ color: '#721C24',
+ textAlign: 'center',
+ },
+ emptyContainer: {
+ padding: spacing.xl,
+ alignItems: 'center',
+ },
+ emptyText: {
+ color: colors.textSecondary,
+ fontSize: 16,
+ },
+});
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add frontend-mobile/app/\(tabs\)/index.tsx
+git commit -m "feat: implement home screen with medicine search"
+```
+
+---
+
+## Task 9: Implement Medicine Detail Screen
+
+**Files:**
+- Create: `frontend-mobile/app/medicine/[id].tsx`
+
+- [ ] **Step 1: Create medicine detail screen**
+
+```typescript
+// frontend-mobile/app/medicine/[id].tsx
+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 { getMedicine, getMedicinePharmacies } from '../../services/medicines';
+import { StockBadge } from '../../components/StockBadge';
+import { LoadingSpinner } from '../../components/LoadingSpinner';
+import { colors, spacing, borderRadius } from '../../constants/theme';
+import { Medicine, PharmacyMedicine } from '../../types';
+
+export default function MedicineDetailScreen() {
+ const { id } = useLocalSearchParams<{ id: string }>();
+ const router = useRouter();
+ const [medicine, setMedicine] = useState(null);
+ const [pharmacies, setPharmacies] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ if (!id) return;
+
+ const fetchMedicine = async () => {
+ try {
+ const [medData, pharmData] = await Promise.all([
+ getMedicine(id),
+ getMedicinePharmacies(id),
+ ]);
+ setMedicine(medData);
+ setPharmacies(pharmData);
+ } catch (error) {
+ console.error('Error fetching medicine:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchMedicine();
+ }, [id]);
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (!medicine) {
+ return (
+
+ Medicamento no encontrado
+
+ );
+ }
+
+ return (
+
+
+ {medicine.nombre}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Farmacias ({pharmacies.length})
+
+
+ {pharmacies.length === 0 ? (
+ No hay farmacias con este medicamento
+ ) : (
+ pharmacies.map((pharm) => (
+ router.push(`/pharmacy/${pharm.pharmacy_id}`)}
+ >
+
+ {pharm.pharmacy?.name}
+ {pharm.pharmacy?.address}
+
+
+ {pharm.price.toFixed(2)} €
+ Stock: {pharm.stock}
+
+
+ ))
+ )}
+
+
+ );
+}
+
+function InfoRow({ label, value }: { label: string; value: string }) {
+ return (
+
+ {label}
+ {value}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
+ padding: spacing.md,
+ backgroundColor: colors.card,
+ },
+ name: {
+ flex: 1,
+ fontSize: 22,
+ fontWeight: 'bold',
+ color: colors.text,
+ marginRight: spacing.sm,
+ },
+ infoSection: {
+ backgroundColor: colors.card,
+ marginTop: spacing.sm,
+ padding: spacing.md,
+ },
+ infoRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ paddingVertical: spacing.sm,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.separator,
+ },
+ infoLabel: {
+ fontSize: 14,
+ color: colors.textSecondary,
+ },
+ infoValue: {
+ fontSize: 14,
+ color: colors.text,
+ fontWeight: '500',
+ },
+ pharmaciesSection: {
+ marginTop: spacing.sm,
+ padding: spacing.md,
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: colors.text,
+ marginBottom: spacing.md,
+ },
+ noPharmacies: {
+ color: colors.textSecondary,
+ textAlign: 'center',
+ padding: spacing.xl,
+ },
+ pharmacyCard: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ backgroundColor: colors.card,
+ borderRadius: borderRadius.md,
+ padding: spacing.md,
+ marginBottom: spacing.sm,
+ },
+ pharmacyInfo: {
+ flex: 1,
+ },
+ pharmacyName: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: colors.text,
+ },
+ pharmacyAddress: {
+ fontSize: 14,
+ color: colors.textSecondary,
+ marginTop: spacing.xs,
+ },
+ pharmacyStock: {
+ alignItems: 'flex-end',
+ },
+ price: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: colors.primary,
+ },
+ stock: {
+ fontSize: 12,
+ color: colors.textSecondary,
+ marginTop: spacing.xs,
+ },
+ errorContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ errorText: {
+ fontSize: 16,
+ color: colors.textSecondary,
+ },
+});
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add frontend-mobile/app/medicine/
+git commit -m "feat: implement medicine detail screen with pharmacies list"
+```
+
+---
+
+## Task 10: Implement Map Screen
+
+**Files:**
+- Modify: `frontend-mobile/app/(tabs)/map.tsx`
+
+- [ ] **Step 1: Install react-native-maps types**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder/frontend-mobile
+npm install --save-dev @types/react-native-maps
+```
+
+- [ ] **Step 2: Implement map screen**
+
+```typescript
+// frontend-mobile/app/(tabs)/map.tsx
+import React, { useEffect, useState } from 'react';
+import { View, StyleSheet, Text } from 'react-native';
+import MapView, { Marker } from 'react-native-maps';
+import { useRouter } from 'expo-router';
+import { getPharmacies } from '../../services/pharmacies';
+import { LoadingSpinner } from '../../components/LoadingSpinner';
+import { colors, spacing } from '../../constants/theme';
+import { Pharmacy } from '../../types';
+
+export default function MapScreen() {
+ const router = useRouter();
+ const [pharmacies, setPharmacies] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [region, setRegion] = useState({
+ latitude: 40.4168, // Madrid default
+ longitude: -3.7038,
+ latitudeDelta: 0.0922,
+ longitudeDelta: 0.0421,
+ });
+
+ useEffect(() => {
+ const fetchPharmacies = async () => {
+ try {
+ const data = await getPharmacies();
+ setPharmacies(data);
+
+ // Center map on first pharmacy if available
+ if (data.length > 0) {
+ setRegion({
+ latitude: data[0].latitude,
+ longitude: data[0].longitude,
+ latitudeDelta: 0.0922,
+ longitudeDelta: 0.0421,
+ });
+ }
+ } catch (error) {
+ console.error('Error fetching pharmacies:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchPharmacies();
+ }, []);
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+
+ {pharmacies.map((pharmacy) => (
+ router.push(`/pharmacy/${pharmacy.id}`)}
+ />
+ ))}
+
+
+
+
+ {pharmacies.length} farmacias en el mapa
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ map: {
+ flex: 1,
+ },
+ legend: {
+ position: 'absolute',
+ bottom: spacing.lg,
+ left: spacing.md,
+ right: spacing.md,
+ backgroundColor: colors.card,
+ borderRadius: 8,
+ padding: spacing.sm,
+ alignItems: 'center',
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ elevation: 5,
+ },
+ legendText: {
+ fontSize: 14,
+ color: colors.text,
+ },
+});
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add frontend-mobile/app/\(tabs\)/map.tsx
+git commit -m "feat: implement map screen with pharmacy markers"
+```
+
+---
+
+## Task 11: Implement Pharmacy Detail Screen
+
+**Files:**
+- Create: `frontend-mobile/app/pharmacy/[id].tsx`
+
+- [ ] **Step 1: Create pharmacy detail screen**
+
+```typescript
+// frontend-mobile/app/pharmacy/[id].tsx
+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 { getPharmacy, getPharmacyMedicines } from '../../services/pharmacies';
+import { LoadingSpinner } from '../../components/LoadingSpinner';
+import { colors, spacing, borderRadius } from '../../constants/theme';
+import { Pharmacy, PharmacyMedicine } from '../../types';
+
+export default function PharmacyDetailScreen() {
+ const { id } = useLocalSearchParams<{ id: string }>();
+ const router = useRouter();
+ const [pharmacy, setPharmacy] = useState(null);
+ const [medicines, setMedicines] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ if (!id) return;
+
+ const fetchPharmacy = async () => {
+ try {
+ const [pharmData, medData] = await Promise.all([
+ getPharmacy(Number(id)),
+ getPharmacyMedicines(Number(id)),
+ ]);
+ setPharmacy(pharmData);
+ setMedicines(medData);
+ } catch (error) {
+ console.error('Error fetching pharmacy:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchPharmacy();
+ }, [id]);
+
+ const handleCall = () => {
+ if (pharmacy?.phone) {
+ Linking.openURL(`tel:${pharmacy.phone}`);
+ }
+ };
+
+ const handleDirections = () => {
+ if (pharmacy) {
+ const url = `https://www.google.com/maps/dir/?api=1&destination=${pharmacy.latitude},${pharmacy.longitude}`;
+ Linking.openURL(url);
+ }
+ };
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (!pharmacy) {
+ return (
+
+ Farmacia no encontrada
+
+ );
+ }
+
+ return (
+
+
+ {pharmacy.name}
+
+
+
+
+
+ Llamar
+
+
+
+
+ Cómo llegar
+
+
+
+
+
+
+ {pharmacy.address}
+
+
+ {pharmacy.phone && (
+
+
+ {pharmacy.phone}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ Medicamentos ({medicines.length})
+
+
+ {medicines.length === 0 ? (
+ No hay medicamentos disponibles
+ ) : (
+ medicines.map((med) => (
+ router.push(`/medicine/${med.medicine_nregistro}`)}
+ >
+
+ {med.medicine_name}
+ Reg: {med.medicine_nregistro}
+
+
+ {med.price.toFixed(2)} €
+ Stock: {med.stock}
+
+
+ ))
+ )}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ header: {
+ padding: spacing.md,
+ backgroundColor: colors.card,
+ },
+ name: {
+ fontSize: 22,
+ fontWeight: 'bold',
+ color: colors.text,
+ },
+ actionsRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ padding: spacing.md,
+ backgroundColor: colors.card,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.separator,
+ },
+ actionButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: spacing.sm,
+ },
+ actionText: {
+ marginLeft: spacing.xs,
+ color: colors.primary,
+ fontWeight: '500',
+ },
+ infoSection: {
+ padding: spacing.md,
+ backgroundColor: colors.card,
+ marginTop: spacing.sm,
+ },
+ infoRow: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ marginBottom: spacing.sm,
+ },
+ infoText: {
+ flex: 1,
+ marginLeft: spacing.sm,
+ fontSize: 16,
+ color: colors.text,
+ },
+ mapContainer: {
+ height: 200,
+ marginTop: spacing.sm,
+ },
+ map: {
+ flex: 1,
+ },
+ medicinesSection: {
+ marginTop: spacing.sm,
+ padding: spacing.md,
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: colors.text,
+ marginBottom: spacing.md,
+ },
+ noMedicines: {
+ color: colors.textSecondary,
+ textAlign: 'center',
+ padding: spacing.xl,
+ },
+ medicineCard: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ backgroundColor: colors.card,
+ borderRadius: borderRadius.md,
+ padding: spacing.md,
+ marginBottom: spacing.sm,
+ },
+ medicineInfo: {
+ flex: 1,
+ },
+ medicineName: {
+ fontSize: 16,
+ fontWeight: '500',
+ color: colors.text,
+ },
+ medicineNregistro: {
+ fontSize: 12,
+ color: colors.textSecondary,
+ marginTop: spacing.xs,
+ },
+ medicineStock: {
+ alignItems: 'flex-end',
+ },
+ price: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: colors.primary,
+ },
+ stock: {
+ fontSize: 12,
+ color: colors.textSecondary,
+ marginTop: spacing.xs,
+ },
+ errorContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ errorText: {
+ fontSize: 16,
+ color: colors.textSecondary,
+ },
+});
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add frontend-mobile/app/pharmacy/
+git commit -m "feat: implement pharmacy detail screen with map and medicines"
+```
+
+---
+
+## Task 12: Implement Auth Screens
+
+**Files:**
+- Create: `frontend-mobile/app/auth/login.tsx`
+- Create: `frontend-mobile/app/auth/register.tsx`
+- Modify: `frontend-mobile/app/(tabs)/profile.tsx`
+
+- [ ] **Step 1: Create login screen**
+
+```typescript
+// frontend-mobile/app/auth/login.tsx
+import React, { useState } from 'react';
+import {
+ View,
+ Text,
+ TextInput,
+ TouchableOpacity,
+ StyleSheet,
+ KeyboardAvoidingView,
+ Platform,
+ Alert
+} from 'react-native';
+import { useRouter } from 'expo-router';
+import { useAuthStore } from '../../store/authStore';
+import { colors, spacing, borderRadius } from '../../constants/theme';
+
+export default function LoginScreen() {
+ const router = useRouter();
+ const { login } = useAuthStore();
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleLogin = async () => {
+ if (!username || !password) {
+ Alert.alert('Error', 'Por favor ingresa usuario y contraseña');
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ await login(username, password);
+ router.replace('/(tabs)');
+ } catch (error) {
+ Alert.alert('Error', 'Credenciales incorrectas');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+ Iniciar Sesión
+ Ingresa tus credenciales para continuar
+
+
+ Usuario
+
+
+
+
+ Contraseña
+
+
+
+
+
+ {isLoading ? 'Ingresando...' : 'Iniciar Sesión'}
+
+
+
+ router.push('/auth/register')}
+ >
+ ¿No tienes cuenta? Regístrate
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ form: {
+ flex: 1,
+ justifyContent: 'center',
+ padding: spacing.xl,
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: 'bold',
+ color: colors.text,
+ marginBottom: spacing.sm,
+ },
+ subtitle: {
+ fontSize: 16,
+ color: colors.textSecondary,
+ marginBottom: spacing.xl,
+ },
+ inputContainer: {
+ marginBottom: spacing.md,
+ },
+ label: {
+ fontSize: 14,
+ fontWeight: '500',
+ color: colors.text,
+ marginBottom: spacing.xs,
+ },
+ input: {
+ backgroundColor: colors.card,
+ borderRadius: borderRadius.md,
+ padding: spacing.md,
+ fontSize: 16,
+ color: colors.text,
+ },
+ button: {
+ backgroundColor: colors.primary,
+ borderRadius: borderRadius.md,
+ padding: spacing.md,
+ alignItems: 'center',
+ marginTop: spacing.md,
+ },
+ buttonDisabled: {
+ opacity: 0.6,
+ },
+ buttonText: {
+ color: colors.textInverse,
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ linkButton: {
+ marginTop: spacing.lg,
+ alignItems: 'center',
+ },
+ linkText: {
+ color: colors.primary,
+ fontSize: 14,
+ },
+});
+```
+
+- [ ] **Step 2: Create register screen**
+
+```typescript
+// frontend-mobile/app/auth/register.tsx
+import React, { useState } from 'react';
+import {
+ View,
+ Text,
+ TextInput,
+ TouchableOpacity,
+ StyleSheet,
+ KeyboardAvoidingView,
+ Platform,
+ Alert
+} from 'react-native';
+import { useRouter } from 'expo-router';
+import { register } from '../../services/auth';
+import { colors, spacing, borderRadius } from '../../constants/theme';
+
+export default function RegisterScreen() {
+ const router = useRouter();
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleRegister = async () => {
+ if (!username || !password || !confirmPassword) {
+ Alert.alert('Error', 'Por favor completa todos los campos');
+ return;
+ }
+
+ if (password !== confirmPassword) {
+ Alert.alert('Error', 'Las contraseñas no coinciden');
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ await register(username, password);
+ router.replace('/(tabs)');
+ } catch (error) {
+ Alert.alert('Error', 'No se pudo crear la cuenta');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+ Crear Cuenta
+ Regístrate para empezar
+
+
+ Usuario
+
+
+
+
+ Contraseña
+
+
+
+
+ Confirmar Contraseña
+
+
+
+
+
+ {isLoading ? 'Creando cuenta...' : 'Crear Cuenta'}
+
+
+
+ router.back()}
+ >
+ ¿Ya tienes cuenta? Inicia sesión
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ form: {
+ flex: 1,
+ justifyContent: 'center',
+ padding: spacing.xl,
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: 'bold',
+ color: colors.text,
+ marginBottom: spacing.sm,
+ },
+ subtitle: {
+ fontSize: 16,
+ color: colors.textSecondary,
+ marginBottom: spacing.xl,
+ },
+ inputContainer: {
+ marginBottom: spacing.md,
+ },
+ label: {
+ fontSize: 14,
+ fontWeight: '500',
+ color: colors.text,
+ marginBottom: spacing.xs,
+ },
+ input: {
+ backgroundColor: colors.card,
+ borderRadius: borderRadius.md,
+ padding: spacing.md,
+ fontSize: 16,
+ color: colors.text,
+ },
+ button: {
+ backgroundColor: colors.primary,
+ borderRadius: borderRadius.md,
+ padding: spacing.md,
+ alignItems: 'center',
+ marginTop: spacing.md,
+ },
+ buttonDisabled: {
+ opacity: 0.6,
+ },
+ buttonText: {
+ color: colors.textInverse,
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ linkButton: {
+ marginTop: spacing.lg,
+ alignItems: 'center',
+ },
+ linkText: {
+ color: colors.primary,
+ fontSize: 14,
+ },
+});
+```
+
+- [ ] **Step 3: Update profile screen**
+
+```typescript
+// frontend-mobile/app/(tabs)/profile.tsx
+import React from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
+import { useRouter } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { useAuth } from '../../hooks/useAuth';
+import { colors, spacing, borderRadius } from '../../constants/theme';
+
+export default function ProfileScreen() {
+ const router = useRouter();
+ const { user, isAuthenticated, isLoading, logout, isAdmin } = useAuth();
+
+ const handleLogout = async () => {
+ Alert.alert(
+ 'Cerrar Sesión',
+ '¿Estás seguro que deseas cerrar sesión?',
+ [
+ { text: 'Cancelar', style: 'cancel' },
+ {
+ text: 'Cerrar Sesión',
+ style: 'destructive',
+ onPress: async () => {
+ await logout();
+ router.replace('/auth/login');
+ }
+ },
+ ]
+ );
+ };
+
+ if (isLoading) {
+ return (
+
+ Cargando...
+
+ );
+ }
+
+ if (!isAuthenticated) {
+ return (
+
+
+
+ Inicia Sesión
+
+ Inicia sesión para acceder a todas las funcionalidades
+
+ router.push('/auth/login')}
+ >
+ Iniciar Sesión
+
+ router.push('/auth/register')}
+ >
+ Crear cuenta nueva
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {user?.username}
+ {isAdmin && Administrador}
+
+
+
+ {isAdmin && (
+
+
+ Panel Admin
+
+
+ )}
+
+
+
+ Favoritos
+
+
+
+
+
+ Notificaciones
+
+
+
+
+
+ Ayuda
+
+
+
+
+
+
+ Cerrar Sesión
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ loadingText: {
+ textAlign: 'center',
+ marginTop: spacing.xxl,
+ color: colors.textSecondary,
+ },
+ authPrompt: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: spacing.xl,
+ },
+ authTitle: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: colors.text,
+ marginTop: spacing.lg,
+ },
+ authSubtitle: {
+ fontSize: 16,
+ color: colors.textSecondary,
+ textAlign: 'center',
+ marginTop: spacing.sm,
+ marginBottom: spacing.xl,
+ },
+ header: {
+ alignItems: 'center',
+ padding: spacing.xl,
+ backgroundColor: colors.card,
+ },
+ avatar: {
+ width: 80,
+ height: 80,
+ borderRadius: 40,
+ backgroundColor: colors.background,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ username: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ color: colors.text,
+ marginTop: spacing.md,
+ },
+ adminBadge: {
+ fontSize: 12,
+ color: colors.primary,
+ backgroundColor: '#E3F2FD',
+ paddingHorizontal: spacing.sm,
+ paddingVertical: spacing.xs,
+ borderRadius: borderRadius.sm,
+ marginTop: spacing.sm,
+ },
+ menuSection: {
+ marginTop: spacing.md,
+ backgroundColor: colors.card,
+ },
+ menuItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: spacing.md,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.separator,
+ },
+ menuText: {
+ flex: 1,
+ marginLeft: spacing.md,
+ fontSize: 16,
+ color: colors.text,
+ },
+ button: {
+ backgroundColor: colors.primary,
+ borderRadius: borderRadius.md,
+ paddingVertical: spacing.md,
+ paddingHorizontal: spacing.xl,
+ },
+ buttonText: {
+ color: colors.textInverse,
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ linkButton: {
+ marginTop: spacing.md,
+ },
+ linkText: {
+ color: colors.primary,
+ fontSize: 14,
+ },
+ logoutButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ margin: spacing.xl,
+ padding: spacing.md,
+ backgroundColor: colors.card,
+ borderRadius: borderRadius.md,
+ borderWidth: 1,
+ borderColor: colors.danger,
+ },
+ logoutText: {
+ marginLeft: spacing.sm,
+ color: colors.danger,
+ fontSize: 16,
+ fontWeight: '500',
+ },
+});
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add frontend-mobile/
+git commit -m "feat: implement auth screens (login, register) and profile screen"
+```
+
+---
+
+## Task 13: Configure Root Layout with Auth Check
+
+**Files:**
+- Modify: `frontend-mobile/app/_layout.tsx`
+
+- [ ] **Step 1: Update root layout with auth provider**
+
+```typescript
+// frontend-mobile/app/_layout.tsx
+import { useEffect } from 'react';
+import { Stack } from 'expo-router';
+import { StatusBar } from 'expo-status-bar';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { GestureHandlerRootView } from 'react-native-gesture-handler';
+import { useAuthStore } from '../store/authStore';
+
+const queryClient = new QueryClient();
+
+export default function RootLayout() {
+ const { checkAuth } = useAuthStore();
+
+ useEffect(() => {
+ checkAuth();
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add frontend-mobile/app/_layout.tsx
+git commit -m "feat: add auth check on app startup"
+```
+
+---
+
+## Task 14: Configure EAS Build
+
+**Files:**
+- Create: `frontend-mobile/eas.json`
+- Create: `frontend-mobile/.gitignore`
+
+- [ ] **Step 1: Create EAS configuration**
+
+```json
+{
+ "cli": {
+ "version": ">= 12.0.0"
+ },
+ "build": {
+ "development": {
+ "developmentClient": true,
+ "distribution": "internal"
+ },
+ "preview": {
+ "distribution": "internal"
+ },
+ "production": {}
+ },
+ "submit": {
+ "production": {
+ "ios": {
+ "appleId": "",
+ "ascAppId": "",
+ "appleTeamId": ""
+ },
+ "android": {
+ "serviceAccountKeyPath": "./google-service-account.json"
+ }
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Create .gitignore for mobile**
+
+```gitignore
+# Expo
+.expo/
+dist/
+web-build/
+
+# Native
+ios/
+android/
+*.jks
+*.p8
+*.p12
+*.key
+*.mobileprovision
+*.orig.*
+
+# Metro
+.metro-health-check*
+
+# debug
+npm-debug.*
+yarn-debug.*
+yarn-error.*
+
+# env files
+.env
+.env.local
+.env.production
+
+# EAS
+eas-cli.json
+
+# Dependencies
+node_modules/
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Build artifacts
+*.apk
+*.aab
+*.ipa
+```
+
+- [ ] **Step 3: Update root package.json with mobile scripts**
+
+```json
+{
+ "scripts": {
+ "dev": "npm-run-all --parallel dev:backend dev:frontend",
+ "dev:backend": "npm run dev --prefix backend",
+ "dev:frontend": "npm run dev --prefix frontend",
+ "dev:mobile": "cd frontend-mobile && npx expo start",
+ "dev:mobile:android": "cd frontend-mobile && npx expo start --android",
+ "dev:mobile:ios": "cd frontend-mobile && npx expo start --ios",
+ "start": "npm-run-all --parallel start:backend start:frontend",
+ "start:backend": "npm start --prefix backend",
+ "start:frontend": "npm run preview --prefix frontend",
+ "install:all": "npm install && npm install --prefix backend && npm install --prefix frontend && npm install --prefix frontend-mobile",
+ "build:web": "npm run build --prefix frontend",
+ "build:mobile": "cd frontend-mobile && eas build",
+ "submit:android": "cd frontend-mobile && eas submit --platform android",
+ "submit:ios": "cd frontend-mobile && eas submit --platform ios",
+ "cap:sync": "npm run build:web && cap sync",
+ "cap:copy": "npm run build:web && cap copy",
+ "cap:open:android": "cap open android",
+ "cap:open:ios": "cap open ios",
+ "cap:run:android": "npm run build:web && cap run android",
+ "cap:run:ios": "npm run build:web && cap run ios"
+ }
+}
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add frontend-mobile/eas.json frontend-mobile/.gitignore package.json
+git commit -m "feat: configure EAS Build and add mobile scripts"
+```
+
+---
+
+## Task 15: Add Push Notifications (Native Extra)
+
+**Files:**
+- Create: `frontend-mobile/services/notifications.ts`
+- Modify: `frontend-mobile/app/_layout.tsx`
+
+- [ ] **Step 1: Create notifications service**
+
+```typescript
+// frontend-mobile/services/notifications.ts
+import * as Notifications from 'expo-notifications';
+import * as Device from 'expo-device';
+import { Platform } from 'react-native';
+
+Notifications.setNotificationHandler({
+ handleNotification: async () => ({
+ shouldShowAlert: true,
+ shouldPlaySound: true,
+ shouldSetBadge: true,
+ }),
+});
+
+export async function registerForPushNotifications() {
+ if (!Device.isDevice) {
+ console.log('Push notifications require a physical device');
+ return null;
+ }
+
+ const { status: existingStatus } = await Notifications.getPermissionsAsync();
+ let finalStatus = existingStatus;
+
+ if (existingStatus !== 'granted') {
+ const { status } = await Notifications.requestPermissionsAsync();
+ finalStatus = status;
+ }
+
+ if (finalStatus !== 'granted') {
+ console.log('Failed to get push token for push notification');
+ return null;
+ }
+
+ const token = await Notifications.getExpoPushTokenAsync();
+
+ if (Platform.OS === 'android') {
+ Notifications.setNotificationChannelAsync('default', {
+ name: 'default',
+ importance: Notifications.AndroidImportance.MAX,
+ vibrationPattern: [0, 250, 250, 250],
+ lightColor: '#007AFF',
+ });
+ }
+
+ return token.data;
+}
+
+export async function scheduleMedicineAvailabilityNotification(
+ medicineName: string,
+ pharmacyName: string
+) {
+ await Notifications.scheduleNotificationAsync({
+ content: {
+ title: 'Medicamento disponible',
+ body: `${medicineName} está disponible en ${pharmacyName}`,
+ data: { type: 'medicine_availability' },
+ },
+ trigger: null, // Immediate
+ });
+}
+
+export async function addNotificationListener(
+ handler: (notification: Notifications.Notification) => void
+) {
+ return Notifications.addNotificationReceivedListener(handler);
+}
+
+export async function addNotificationResponseListener(
+ handler: (response: Notifications.NotificationResponse) => void
+) {
+ return Notifications.addNotificationResponseReceivedListener(handler);
+}
+```
+
+- [ ] **Step 2: Update root layout to initialize notifications**
+
+```typescript
+// frontend-mobile/app/_layout.tsx (add to imports and useEffect)
+import { useEffect, useRef } from 'react';
+import { Stack } from 'expo-router';
+import { StatusBar } from 'expo-status-bar';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { GestureHandlerRootView } from 'react-native-gesture-handler';
+import * as Notifications from 'expo-notifications';
+import { useAuthStore } from '../store/authStore';
+import { registerForPushNotifications, addNotificationListener, addNotificationResponseListener } from '../services/notifications';
+
+const queryClient = new QueryClient();
+
+export default function RootLayout() {
+ const { checkAuth } = useAuthStore();
+ const notificationListener = useRef();
+ const responseListener = useRef();
+
+ useEffect(() => {
+ checkAuth();
+
+ // Setup push notifications
+ registerForPushNotifications();
+
+ notificationListener.current = addNotificationListener((notification) => {
+ console.log('Notification received:', notification);
+ });
+
+ responseListener.current = addNotificationResponseListener((response) => {
+ console.log('Notification clicked:', response);
+ });
+
+ return () => {
+ if (notificationListener.current) {
+ Notifications.removeNotificationSubscription(notificationListener.current);
+ }
+ if (responseListener.current) {
+ Notifications.removeNotificationSubscription(responseListener.current);
+ }
+ };
+ }, []);
+
+ // ... rest of component remains the same
+}
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add frontend-mobile/
+git commit -m "feat: add push notifications support"
+```
+
+---
+
+## Task 16: Add Barcode Scanner (Native Extra)
+
+**Files:**
+- Create: `frontend-mobile/components/BarcodeScanner.tsx`
+- Create: `frontend-mobile/app/scanner.tsx`
+
+- [ ] **Step 1: Create barcode scanner component**
+
+```typescript
+// frontend-mobile/components/BarcodeScanner.tsx
+import React, { useState, useEffect } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
+import { CameraView, CameraType, useCameraPermissions } from 'expo-camera';
+import { Ionicons } from '@expo/vector-icons';
+import { colors, spacing, borderRadius } from '../constants/theme';
+
+interface BarcodeScannerProps {
+ onBarcodeScanned: (barcode: string) => void;
+ onClose: () => void;
+}
+
+export function BarcodeScanner({ onBarcodeScanned, onClose }: BarcodeScannerProps) {
+ const [permission, requestPermission] = useCameraPermissions();
+ const [scanned, setScanned] = useState(false);
+
+ if (!permission) {
+ return ;
+ }
+
+ if (!permission.granted) {
+ return (
+
+
+ Permiso de cámara requerido
+
+ Necesitamos acceso a la cámara para escanear códigos de barras
+
+
+ Conceder permiso
+
+
+ Cancelar
+
+
+ );
+ }
+
+ const handleBarCodeScanned = ({ type, data }: { type: string; data: string }) => {
+ if (scanned) return;
+ setScanned(true);
+ onBarcodeScanned(data);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ Apunta la cámara al código de barras del medicamento
+
+
+
+
+
+
+
+ {scanned && (
+
+ setScanned(false)}
+ >
+ Escanear de nuevo
+
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#000',
+ },
+ permissionContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: colors.background,
+ padding: spacing.xl,
+ },
+ permissionTitle: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ color: colors.text,
+ marginTop: spacing.lg,
+ },
+ permissionText: {
+ fontSize: 16,
+ color: colors.textSecondary,
+ textAlign: 'center',
+ marginTop: spacing.sm,
+ marginBottom: spacing.xl,
+ },
+ permissionButton: {
+ backgroundColor: colors.primary,
+ borderRadius: borderRadius.md,
+ paddingVertical: spacing.md,
+ paddingHorizontal: spacing.xl,
+ },
+ permissionButtonText: {
+ color: colors.textInverse,
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ cancelButton: {
+ marginTop: spacing.md,
+ },
+ cancelButtonText: {
+ color: colors.textSecondary,
+ fontSize: 14,
+ },
+ overlay: {
+ flex: 1,
+ backgroundColor: 'transparent',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ scannerFrame: {
+ width: 250,
+ height: 250,
+ borderWidth: 2,
+ borderColor: 'transparent',
+ position: 'relative',
+ },
+ corner: {
+ position: 'absolute',
+ width: 30,
+ height: 30,
+ borderColor: colors.primary,
+ },
+ topLeft: {
+ top: 0,
+ left: 0,
+ borderTopWidth: 3,
+ borderLeftWidth: 3,
+ },
+ topRight: {
+ top: 0,
+ right: 0,
+ borderTopWidth: 3,
+ borderRightWidth: 3,
+ },
+ bottomLeft: {
+ bottom: 0,
+ left: 0,
+ borderBottomWidth: 3,
+ borderLeftWidth: 3,
+ },
+ bottomRight: {
+ bottom: 0,
+ right: 0,
+ borderBottomWidth: 3,
+ borderRightWidth: 3,
+ },
+ instruction: {
+ color: colors.textInverse,
+ fontSize: 14,
+ textAlign: 'center',
+ marginTop: spacing.xl,
+ backgroundColor: 'rgba(0,0,0,0.6)',
+ paddingHorizontal: spacing.md,
+ paddingVertical: spacing.sm,
+ borderRadius: borderRadius.sm,
+ },
+ closeButton: {
+ position: 'absolute',
+ top: spacing.xl,
+ right: spacing.xl,
+ backgroundColor: 'rgba(0,0,0,0.6)',
+ borderRadius: 20,
+ padding: spacing.sm,
+ },
+ scannedOverlay: {
+ position: 'absolute',
+ bottom: spacing.xxl,
+ left: spacing.xl,
+ right: spacing.xl,
+ },
+ scanAgainButton: {
+ backgroundColor: colors.primary,
+ borderRadius: borderRadius.md,
+ padding: spacing.md,
+ alignItems: 'center',
+ },
+ scanAgainText: {
+ color: colors.textInverse,
+ fontSize: 16,
+ fontWeight: '600',
+ },
+});
+```
+
+- [ ] **Step 2: Create scanner screen**
+
+```typescript
+// frontend-mobile/app/scanner.tsx
+import React, { useState } from 'react';
+import { View, StyleSheet } from 'react-native';
+import { useRouter } from 'expo-router';
+import { BarcodeScanner } from '../components/BarcodeScanner';
+import { searchMedicines } from '../services/medicines';
+
+export default function ScannerScreen() {
+ const router = useRouter();
+ const [isSearching, setIsSearching] = useState(false);
+
+ const handleBarcodeScanned = async (barcode: string) => {
+ setIsSearching(true);
+ try {
+ // Try to find medicine by barcode/registration number
+ const results = await searchMedicines(barcode);
+ if (results.length > 0) {
+ router.push(`/medicine/${results[0].nregistro}`);
+ } else {
+ // Fallback: try direct lookup
+ router.push(`/medicine/${barcode}`);
+ }
+ } catch (error) {
+ console.error('Error searching medicine:', error);
+ router.push(`/medicine/${barcode}`);
+ } finally {
+ setIsSearching(false);
+ }
+ };
+
+ const handleClose = () => {
+ router.back();
+ };
+
+ return (
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+});
+```
+
+- [ ] **Step 3: Add scanner button to home screen**
+
+```typescript
+// Add to frontend-mobile/app/(tabs)/index.tsx - add import and button
+import { Ionicons } from '@expo/vector-icons';
+import { TouchableOpacity } from 'react-native';
+
+// Add this button next to SearchBar
+ router.push('/scanner')}
+>
+
+
+
+// Add to styles
+scannerButton: {
+ position: 'absolute',
+ right: spacing.md,
+ top: spacing.md,
+ backgroundColor: colors.card,
+ borderRadius: 20,
+ padding: spacing.sm,
+},
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add frontend-mobile/
+git commit -m "feat: add barcode scanner for medicine lookup"
+```
+
+---
+
+## Task 17: Add Biometrics (Native Extra)
+
+**Files:**
+- Create: `frontend-mobile/services/biometrics.ts`
+- Modify: `frontend-mobile/app/auth/login.tsx`
+
+- [ ] **Step 1: Create biometrics service**
+
+```typescript
+// frontend-mobile/services/biometrics.ts
+import * as LocalAuthentication from 'expo-local-authentication';
+import * as SecureStore from 'expo-secure-store';
+
+export async function isBiometricsAvailable(): Promise {
+ const compatible = await LocalAuthentication.hasHardwareAsync();
+ const enrolled = await LocalAuthentication.isEnrolledAsync();
+ return compatible && enrolled;
+}
+
+export async function authenticateWithBiometrics(): Promise {
+ try {
+ const result = await LocalAuthentication.authenticateAsync({
+ promptMessage: 'Inicia sesión con biometría',
+ cancelLabel: 'Cancelar',
+ disableDeviceFallback: false,
+ });
+ return result.success;
+ } catch (error) {
+ console.error('Biometric authentication error:', error);
+ return false;
+ }
+}
+
+export async function saveBiometricCredentials(username: string): Promise {
+ await SecureStore.setItemAsync('biometric_username', username);
+}
+
+export async function getBiometricUsername(): Promise {
+ return await SecureStore.getItemAsync('biometric_username');
+}
+
+export async function clearBiometricCredentials(): Promise {
+ await SecureStore.deleteItemAsync('biometric_username');
+}
+```
+
+- [ ] **Step 2: Update login screen with biometrics**
+
+```typescript
+// Add to frontend-mobile/app/auth/login.tsx
+import {
+ isBiometricsAvailable,
+ authenticateWithBiometrics,
+ saveBiometricCredentials,
+ getBiometricUsername
+} from '../../services/biometrics';
+
+// Add state
+const [biometricsAvailable, setBiometricsAvailable] = useState(false);
+const [biometricUsername, setBiometricUsername] = useState(null);
+
+// Add useEffect
+useEffect(() => {
+ checkBiometrics();
+}, []);
+
+const checkBiometrics = async () => {
+ const available = await isBiometricsAvailable();
+ setBiometricsAvailable(available);
+ if (available) {
+ const savedUsername = await getBiometricUsername();
+ setBiometricUsername(savedUsername);
+ }
+};
+
+const handleBiometricLogin = async () => {
+ const success = await authenticateWithBiometrics();
+ if (success && biometricUsername) {
+ setIsLoading(true);
+ try {
+ // Use stored password or implement token-based auth
+ await login(biometricUsername, ''); // You may need to adjust this
+ router.replace('/(tabs)');
+ } catch (error) {
+ Alert.alert('Error', 'Error al autenticar con biometría');
+ } finally {
+ setIsLoading(false);
+ }
+ }
+};
+
+// After successful login, save for biometrics
+const handleLogin = async () => {
+ // ... existing login logic
+ if (success) {
+ await saveBiometricCredentials(username);
+ }
+};
+
+// Add biometric button in the form
+{biometricsAvailable && biometricUsername && (
+
+
+
+ Iniciar sesión con biometría
+
+
+)}
+
+// Add styles
+biometricButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginTop: spacing.lg,
+ padding: spacing.md,
+ backgroundColor: colors.background,
+ borderRadius: borderRadius.md,
+ borderWidth: 1,
+ borderColor: colors.primary,
+},
+biometricText: {
+ marginLeft: spacing.sm,
+ color: colors.primary,
+ fontSize: 16,
+ fontWeight: '500',
+},
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add frontend-mobile/
+git commit -m "feat: add biometric authentication support"
+```
+
+---
+
+## Task 18: Test and Verify
+
+**Files:**
+- None (testing only)
+
+- [ ] **Step 1: Run Expo development server**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder/frontend-mobile
+npx expo start --clear
+```
+
+Expected: Dev server starts, QR code displayed
+
+- [ ] **Step 2: Test on iOS Simulator (if Mac)**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder/frontend-mobile
+npx expo start --ios
+```
+
+Expected: App launches in simulator
+
+- [ ] **Step 3: Test on Android Emulator**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder/frontend-mobile
+npx expo start --android
+```
+
+Expected: App launches in emulator
+
+- [ ] **Step 4: Verify navigation**
+
+- [ ] Tab navigation works (Home, Map, Profile)
+- [ ] Medicine search returns results
+- [ ] Medicine detail shows pharmacies
+- [ ] Map shows pharmacy markers
+- [ ] Pharmacy detail shows medicines
+- [ ] Login/logout works
+
+- [ ] **Step 5: Final commit**
+
+```bash
+cd /home/f80ans0/Projects/Webs/FarmaFinder
+git add .
+git commit -m "feat: complete FarmaFinder Mobile MVP with native extras"
+```
+
+---
+
+## Summary
+
+| Task | Description | Files |
+|------|-------------|-------|
+| 1 | Initialize Expo project | package.json, app.json, tsconfig.json, babel.config.js |
+| 2 | Setup navigation | app/_layout.tsx, (tabs)/_layout.tsx, screen files |
+| 3 | Create types | types/index.ts |
+| 4 | Create API services | services/api.ts, medicines.ts, pharmacies.ts, auth.ts |
+| 5 | Create state management | store/authStore.ts, searchStore.ts, hooks/useAuth.ts |
+| 6 | Create theme | constants/theme.ts |
+| 7 | Create UI components | components/*.tsx |
+| 8 | Implement Home screen | app/(tabs)/index.tsx |
+| 9 | Implement Medicine detail | app/medicine/[id].tsx |
+| 10 | Implement Map screen | app/(tabs)/map.tsx |
+| 11 | Implement Pharmacy detail | app/pharmacy/[id].tsx |
+| 12 | Implement Auth screens | app/auth/login.tsx, register.tsx, profile.tsx |
+| 13 | Configure root layout | app/_layout.tsx |
+| 14 | Configure EAS Build | eas.json, .gitignore, package.json |
+| 15 | Add Push Notifications | services/notifications.ts |
+| 16 | Add Barcode Scanner | components/BarcodeScanner.tsx, app/scanner.tsx |
+| 17 | Add Biometrics | services/biometrics.ts, login.tsx |
+| 18 | Test and verify | Manual testing |
+
+---
+
+## Next Steps After Implementation
+
+1. **Configure EAS Build**: Set up Expo account and EAS
+2. **Test on real devices**: Use Expo Go or development build
+3. **Configure push notifications**: Set up FCM/APNs
+4. **Add app icons and splash screens**: Design assets
+5. **Configure deep links**: Set up URL scheme
+6. **Submit to App Store**: Follow Apple guidelines
+7. **Submit to Google Play**: Follow Google guidelines