# MeowSpool Mobile — Agent Guide ## Visão Geral Aplicação mobile do **MeowSpool** construída com **React Native** + **Expo** (SDK ~51), usando **expo-router** para navegação baseada em sistema de arquivos. A arquitetura espelha o backend Rust: **Clean Code / Hexagonal**, com camadas bem delimitadas do domínio até a apresentação. O app é **offline-first**: todos os dados são persistidos localmente em **SQLite** (expo-sqlite + SQLCipher) e sincronizados com o backend Rust em background usando estratégia **last-write-wins** via campo `updatedAt`. --- ## Stack | Componente | Biblioteca | Observação | | ----------------- | ----------------------------------- | --------------------------------------------------------- | | Framework | `expo` ~51 | Managed Workflow | | Navigation | `expo-router` ~3 | File-system routing | | Linguagem | TypeScript 5.x | strict mode | | Node Version | 22 LTS | Gerenciado via `mise` (`.mise.toml`) | | Java Version | 17 | Obrigatório para React Native 0.81 (Java 24 quebra CMake) | | UI | React Native + `@expo/vector-icons` | Ionicons | | Forms | `react-hook-form` + `zod` | Validação em runtime | | Estado global | `zustand` | Stores em `src/store/` | | HTTP | `axios` | Interceptor Bearer + refresh automático | | DB local | `expo-sqlite` + SQLCipher | WAL mode, foreign keys | | Auth persistência | `expo-secure-store` | JWT cifrado no keychain | | Safe Area | `react-native-safe-area-context` | | | Gesture Handler | `react-native-gesture-handler` | | | Animations | `react-native-reanimated` | | | QR Code render | `react-native-qrcode-svg` | Render de QR Code em tela | | Câmera / Scanner | `expo-camera` ~17 | ML Kit barcode scan; requer development build | --- ## Arquitetura de Camadas ``` src/ ├── domain/ ← Entidades puras (sem dependências externas) ├── ports/ ← Interfaces/contratos (traits equivalentes ao Rust) ├── application/ ← Use Cases (orquestram domínio + ports) ├── adapters/ │ ├── remote/ ← Implementações HTTP (Axios → API Rust) │ └── local/ ← Implementações SQLite (expo-sqlite) ├── store/ ← Estado em memória (Zustand) — cache das queries ├── shared/ ← Design tokens, constantes, utilitários └── presentation/ └── components/ ← Componentes UI reutilizáveis ``` ### Regra de dependência ``` presentation → store → application → ports ← adapters ↑ domain ``` Nenhuma camada interna importa de camadas externas. Os `adapters` implementam os `ports`. --- ## Estrutura de Arquivos ``` mobile/ ├── .mise.toml ← node 22 LTS, java 17 ├── app.json ← Expo config, scheme "meowspool" ├── babel.config.js ← module-resolver + reanimated ├── tsconfig.json ← aliases @domain, @ports, @application, │ @adapters, @presentation, @store, @shared ├── index.js ← expo-router entry point ├── package.json │ ├── app/ ← expo-router file-system routes │ ├── _layout.tsx ← Root layout (carrega sessão) │ ├── (auth)/ │ │ ├── _layout.tsx ← Redireciona se já autenticado │ │ ├── login.tsx ← G3-0 │ │ ├── register.tsx ← IX-0 │ │ ├── forgot-password.tsx ← L2-0 │ │ ├── verify-email.tsx ← RU-0 │ │ ├── reset-password.tsx ← 117-0 │ │ └── password-reset-done.tsx ← 135-0 │ └── (app)/ │ ├── _layout.tsx ← Stack autenticado │ ├── (tabs)/ │ │ ├── _layout.tsx ← Bottom tabs: Início|Estoque|[+]|Config|Perfil │ │ ├── home.tsx ← 1-0 Dashboard │ │ ├── inventory.tsx ← CW-0 Inventário │ │ ├── add.tsx ← FAB → redireciona para /inventory/new │ │ ├── config.tsx ← M4-0 Presets de Carretéis │ │ └── profile.tsx ← 1LQ-0 Perfil │ ├── inventory/ │ │ ├── new.tsx ← 2X-0 Cadastro de Filamento │ │ ├── filters.tsx ← 17H-0 Filtros (bottom sheet) │ │ ├── [id].tsx ← 6L-0 Detalhe do Filamento │ │ └── [id]/ │ │ └── edit.tsx ← 13O-0 Editar Filamento │ ├── config/ │ │ └── presets/ │ │ ├── new.tsx ← QM-0 Novo Preset │ │ └── [id]/ │ │ └── edit.tsx ← 1KD-0 Editar Preset │ ├── scanner.tsx ← Scanner de QR Code (expo-camera ML Kit) │ └── filaments/ │ └── [id]/ │ ├── qrcode.tsx ← Ver QR Code (react-native-qrcode-svg) │ └── label.tsx ← Exportar Etiqueta SVG │ └── src/ ├── domain/ │ ├── Filament.ts ← interface Filament, calcNetWeight │ ├── SpoolPreset.ts ← interface SpoolPreset, isUserOwnedPreset │ ├── User.ts ← interface User, AuthSession, DTOs │ └── Dashboard.ts ← DashboardData, MaterialSummary, etc. ├── ports/ │ ├── FilamentRepository.ts ← interface IFilamentRepository │ ├── SpoolPresetRepository.ts ← interface ISpoolPresetRepository │ └── AuthRepository.ts ← interface IAuthRepository ├── application/ │ ├── filament/ │ │ ├── CreateFilamentUseCase.ts │ │ ├── UpdateFilamentUseCase.ts │ │ ├── ListFilamentsUseCase.ts │ │ └── DeleteFilamentUseCase.ts │ ├── preset/ │ │ └── PresetUseCases.ts ← List, Create, Update, Delete │ └── auth/ │ └── AuthUseCases.ts ← Login, Register, Google, Logout, etc. ├── adapters/ │ ├── remote/ │ │ ├── httpClient.ts ← Axios + interceptors JWT + refresh │ │ ├── ApiAuthRepository.ts ← /api/v1/auth/* │ │ ├── ApiFilamentRepository.ts ← /api/v1/filaments/* │ │ └── ApiSpoolPresetRepository.ts ← /api/v1/spool-presets/* │ └── local/ │ ├── database.ts ← init SQLite, WAL, foreign keys, tabelas │ ├── LocalFilamentRepository.ts │ └── LocalSpoolPresetRepository.ts ├── store/ │ ├── authStore.ts ← Zustand: sessão, SecureStore │ ├── filamentStore.ts ← Zustand: lista + filtros em memória │ └── presetStore.ts ← Zustand: systemPresets + userPresets ├── shared/ │ ├── theme.ts ← Design tokens (cores, tipografia, espaçamento) │ ├── constants.ts ← API_BASE_URL, SecureStore keys, MATERIALS │ └── utils/ │ └── filament.ts ← calcFilamentPercentage, formatWeight, etc. └── presentation/ └── components/ ├── ui/ │ ├── Button.tsx ← primary, secondary, ghost, danger │ ├── Input.tsx ← label, leftIcon, rightLabel, isPassword, error │ ├── Card.tsx ← container surface com borda │ └── Badge.tsx ← badge dinâmico de estoque ├── layout/ │ ├── Screen.tsx ← SafeArea + scroll + keyboardAvoiding │ └── Header.tsx ← título centralizado, back, slot direito └── filament/ ├── StockBar.tsx ← barra de progresso + badge numérico ├── ColorSwatch.tsx ← quadrado/círculo com cor hex └── FilamentCard.tsx ← card de lista ``` --- ## Design Tokens Arquivo: `src/shared/theme.ts` ### Paleta (siamês) | Token | Valor | Uso | | --------------- | ----------- | --------------------------------- | | `bgBase` | `#1E1B18` | Fundo geral | | `bgSurface` | `#2A2622` | Cards, containers | | `bgHover` | `#332F2B` | Hover/pressed em cards | | `textPrimary` | `#F5EEDC` | Títulos, peso líquido | | `textSecondary` | `#C9C1B0` | Descrições, labels | | `accent` | `#38BCC2` | Botões de ação, elementos ativos | | `accentMuted` | `#38BCC226` | Background de badges accent (10%) | | `stockLow` | `#FF6B6B` | ≤ 15% — vermelho | | `stockMedium` | `#FF9F43` | ≤ 35% — laranja | | `stockOk` | `#38BCC2` | > 35% — accent | | `border` | `#3D3830` | Bordas sutis | | `error` | `#FF6B6B` | Mensagens de erro | ### Tipografia - UI: **Inter** - Mono (slugs, hex, URLs): **JetBrains Mono** - App é **dark-mode exclusivo** — nenhum suporte a light mode --- ## Navegação (expo-router) ### Estrutura de grupos ``` (auth) ← sem autenticação; redireciona para (app) se sessão válida (app) ← com autenticação; redireciona para (auth) se sem sessão (tabs) ← bottom tabs fixos ``` ### Deep links - Scheme: `meowspool://` - Filamento: `meowspool://filament/` → `/(app)/inventory/` (singular, alinhado com backend) - O scanner (`scanner.tsx`) faz match via `/meowspool:\/\/filament\/([^/]+)/` e navega para `/(app)/inventory/` - O QR Code de cada filamento exibe `meowspool://filament/` usando `react-native-qrcode-svg` --- ## Estratégia Offline-First 1. **Escrita**: toda mutação persiste primeiro no SQLite (`synced = 0`) e enfileira em `sync_queue`. 2. **Leitura**: lê sempre do SQLite; a API é usada apenas para sync. 3. **Sync**: background job consome `sync_queue` e envia para o backend Rust. 4. **Conflito**: last-write-wins via `updatedAt` (ISO 8601). O backend é a fonte de verdade em conflitos. 5. **Auth offline**: token armazenado no `expo-secure-store`; refresh automático via interceptor Axios. ### Schema SQLite ```sql -- filaments id TEXT PRIMARY KEY, user_id TEXT, material TEXT, brand TEXT, model TEXT, color_hex TEXT, spool_preset_id TEXT, total_weight_g REAL, net_weight_g REAL, temp_hotend_c REAL, temp_bed_c REAL, flow_factor_pct REAL, notes TEXT, synced INTEGER DEFAULT 0, updated_at TEXT, created_at TEXT -- spool_presets id TEXT PRIMARY KEY, user_id TEXT, name TEXT, spool_weight_g REAL, is_system INTEGER DEFAULT 0, synced INTEGER DEFAULT 0, created_at TEXT -- sync_queue id INTEGER PRIMARY KEY AUTOINCREMENT, entity_type TEXT, entity_id TEXT, operation TEXT, payload TEXT, created_at TEXT ``` --- ## Convenções de Código ### Nomenclatura - **Componentes React**: PascalCase (`FilamentCard.tsx`) - **Hooks**: camelCase com prefixo `use` (`useFilamentStore`) - **Use Cases**: PascalCase com sufixo `UseCase` (`CreateFilamentUseCase`) - **Repositórios**: PascalCase com prefixo de implementação (`ApiFilamentRepository`, `LocalFilamentRepository`) - **Stores**: camelCase (`filamentStore.ts`), exportados como `useXxxStore` ### Imports Sempre usar aliases em vez de caminhos relativos: ```ts import { Filament } from "@domain/Filament"; import { useFilamentStore } from "@store/filamentStore"; import { colors } from "@shared/theme"; import { Button } from "@presentation/components/ui/Button"; ``` ### Componentes de tela - Toda tela usa `SafeAreaView` com `backgroundColor: colors.bgBase` - Header manual (não usa o header do expo-router): `headerShown: false` - CTA fixo: `View` com `padding: spacing[5]` + `borderTopWidth: 1` + `borderTopColor: colors.border` - ScrollViews com `showsVerticalScrollIndicator={false}` e `keyboardShouldPersistTaps="handled"` ### Formulários - `react-hook-form` + `zodResolver` para todos os formulários - Validações no schema Zod (nunca inline) - `isLoading` local no componente durante submit - Erros de API exibidos via `Alert.alert` --- ## API do Backend (rotas relevantes) Base: `EXPO_PUBLIC_API_URL` (padrão: `http://localhost:3000/api/v1`) ### Auth | Método | Rota | Descrição | | ------ | ----------------------- | --------------------------- | | POST | `/auth/login` | Login email/senha | | POST | `/auth/register` | Cadastro | | POST | `/auth/google` | OAuth Google | | POST | `/auth/refresh` | Refresh token | | POST | `/auth/logout` | Logout | | POST | `/auth/forgot-password` | Solicitar reset de senha | | POST | `/auth/reset-password` | Confirmar reset com token | | POST | `/auth/verify-email` | Verificar e-mail com código | ### Filaments | Método | Rota | Descrição | | ------ | ----------------------- | -------------------- | | GET | `/filaments` | Listar (com filtros) | | POST | `/filaments` | Criar | | GET | `/filaments/:id` | Detalhe | | PATCH | `/filaments/:id` | Atualizar | | DELETE | `/filaments/:id` | Excluir | | GET | `/filaments/:id/qrcode` | QR Code SVG | | GET | `/filaments/:id/label` | Etiqueta SVG | ### Spool Presets | Método | Rota | Descrição | | ------ | -------------------- | --------------------------- | | GET | `/spool-presets` | Listar (sistema + usuário) | | POST | `/spool-presets` | Criar preset do usuário | | PATCH | `/spool-presets/:id` | Atualizar preset do usuário | | DELETE | `/spool-presets/:id` | Excluir preset do usuário | ### Dashboard | Método | Rota | Descrição | | ------ | ------------ | ---------------------------- | | GET | `/dashboard` | Dados agregados do dashboard | --- ## Próximos Passos (integrações pendentes) - [ ] Instanciar e injetar repositórios concretos nos Use Cases (DI container simples ou Context) - [ ] Implementar sync background com `sync_queue` SQLite → API - [x] Integrar `react-native-qrcode-svg` para render real do QR Code - [x] Scanner de QR Code com `expo-camera` v17 (ML Kit) → `scanner.tsx` - [ ] Gerar SVG de etiqueta (integração com `/filaments/:id/label` do backend) - [ ] Expo Notifications para alertas de estoque baixo - [ ] Google OAuth com `expo-auth-session` - [ ] Testes de integração com Jest + Testing Library --- ## Mudanças Recentes (14/03/2026) ### ✅ Implementações Completadas #### 1. **Delete Filament — Integração com API** - **Arquivos**: `app/(app)/inventory/[id].tsx`, `app/(app)/inventory/[id]/edit.tsx` - **Mudança**: `handleDelete()` agora chama `deleteFilamentUseCase.execute()` antes de remover do store local - **Código**: ```typescript const handleDelete = async () => { try { const user = useAuthStore.getState().user; await deleteFilamentUseCase.execute(filament!.id, user?.id); removeFilament(filament!.id); Alert.alert("Sucesso", "Filamento deletado da API"); } catch (error) { Alert.alert("Erro", "Falha ao deletar: " + (error as Error).message); } }; ``` - **Impacto**: Delete agora funciona corretamente em ambos os screens (detail e edit) #### 2. **Pull-to-Refresh — Inventory List** - **Arquivo**: `app/(tabs)/inventory.tsx` - **Mudança**: Adicionado `RefreshControl` ao FlatList com chamada a `listFilamentsUseCase.execute()` - **Código**: ```typescript const [isRefreshing, setIsRefreshing] = useState(false); const onRefresh = async () => { setIsRefreshing(true); try { const user = useAuthStore.getState().user; const result = await listFilamentsUseCase.execute(user?.id); setFilaments(result); } finally { setIsRefreshing(false); } }; } // ... resto do componente /> ``` - **Impacto**: Usuários podem puxar para baixo e sincronizar lista sem reabrir o app #### 3. **Update Filament — Integração com API** - **Arquivo**: `app/(app)/inventory/[id]/edit.tsx` - **Mudança**: `onSubmit()` agora chama `updateFilamentUseCase.execute()` em vez de apenas atualizar o store local - **Código**: ```typescript const onSubmit = async () => { try { const user = useAuthStore.getState().user; const result = await updateFilamentUseCase.execute( { id: filament!.id, brand: formData.brand, model: formData.model, color: formData.color, weight_g: parseFloat(formData.weight_g), }, user?.id, ); updateFilament(result); Alert.alert("Sucesso", "Filamento atualizado na API"); router.back(); } catch (error) { Alert.alert("Erro", "Falha ao atualizar: " + (error as Error).message); } }; ``` - **Impacto**: Edições de filamento agora persistem no servidor #### 4. **Custom Color Picker — Hex Input com Validação** - **Arquivos**: `app/(app)/inventory/new.tsx`, `app/(app)/inventory/[id]/edit.tsx` - **Mudança**: Adicionado `handleOpenColorPicker()` que usa `Alert.prompt()` para capturar hexadecimais - **Validação**: ```typescript const isValidHex = (hex: string): boolean => { return /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(hex); }; const normalizeHex = (hex: string): string => { return hex.startsWith("#") ? hex : `#${hex}`; }; const handleOpenColorPicker = () => { setIsOpeningColorPicker(true); Alert.prompt( "Hex Color", "Enter hex color (e.g., #FF5733 or FF5733)", [ { text: "Cancel", onPress: () => setIsOpeningColorPicker(false), style: "cancel", }, { text: "Add Color", onPress: (input: string | undefined) => { if (input && isValidHex(input)) { setFormData((prev) => ({ ...prev, color: normalizeHex(input) })); } else { Alert.alert("Invalid hex color", "Please enter a valid hex code"); } setIsOpeningColorPicker(false); }, }, ], "plain-text", ); }; ``` - **Integração**: Botão "+" vinculado a `handleOpenColorPicker` - **Impacto**: Usuários podem agora inserir cores customizadas por código hexadecimal #### 5. **Sincronização de Estado — API-First Pattern** - **Padrão**: Todas operações agora seguem: **API call → store update** - **Benefício**: Source of truth centralizada no servidor - **Implementação**: - Delete: `deleteFilamentUseCase` → `removeFilament()` - Update: `updateFilamentUseCase` → `updateFilament(result)` - Refresh: `listFilamentsUseCase` → `setFilaments(result)` - Create: `createFilamentUseCase` → `addFilament(result)` #### 6. **CatIcon com Fundo Turquesa — Design Integration** - **Arquivos**: `src/presentation/components/icons/CatIcon.tsx`, `app/(auth)/login.tsx` - **Mudança**: Implementação de suporte a fundo turquesa no ícone CatIcon conforme design Paper - **Novas Props**: ```typescript interface CatIconProps { size?: number; // tamanho do SVG (padrão: 48px) color?: string; // cor do ícone (padrão: #14120F) withBackground?: boolean; // ativar fundo (padrão: false) backgroundColor?: string; // cor do fundo (padrão: #38BCC2 — accent turquesa) } ``` - **Implementação**: Quando `withBackground={true}`, o componente envolve o SVG em um `View` com: - Tamanho: 80×80px - Background color: `#38BCC2` (accent turquesa) - Border radius: 24px - Flexbox centered - **Uso na tela de Login** (G3-0): ```typescript // Antes: // Depois: ``` - **Remoção**: Deletado `logoContainer` style antigo do `StyleSheet` - **Resultado**: Ícone agora exibe com fundo turquesa arredondado, alinhado com design do Paper - **Impacto**: Visual correto da tela de login (G3-0) sincronizado com mockups ### Arquitetura Mantida - Todos use cases injetados via `container.ts` (Dependency Injection) - Zod schemas usados para validação de formulários - JWT interceptor em Axios automaticamente adiciona `Authorization` header - Error handling com `Alert.alert()` visível ao usuário --- ## Executando o Projeto ```bash # Instalar dependências (na pasta mobile/) mise install # garante Node 22 e Java 17 npm install # Iniciar servidor de desenvolvimento (Expo Go — sem câmera ML Kit) npx expo start # Build nativo Android (necessário para expo-camera ML Kit barcode scan) mise exec -- npx expo run:android # Build nativo iOS mise exec -- npx expo run:ios ``` > **Variável de ambiente**: crie `mobile/.env` com: > > ``` > EXPO_PUBLIC_API_URL=http://localhost:3000/api/v1 > ``` > **Atenção ao build Android**: o projeto usa `namespace "com.meowspool"` no `build.gradle` mas > o código fonte fica em `com.meowspool.app`. Por isso `MainActivity.kt` e `MainApplication.kt` > importam explicitamente `com.meowspool.R` e `com.meowspool.BuildConfig`, e o `AndroidManifest.xml` > usa nomes totalmente qualificados (`com.meowspool.app.MainApplication`, `com.meowspool.app.MainActivity`). > Não usar nomes relativos (`.MainApplication`) no manifest — eles resolvem para `com.meowspool.*` > (sem `.app`), causando `ClassNotFoundException` em runtime.