Files
MeowSpool/mobile/agent.md
T
Felipe d1eabfb690 Add CatIcon component and update app icon
- Introduced a new CatIcon component in TypeScript using React Native and SVG.
- The CatIcon supports customizable size, color, and optional background.
- Updated the app's icon image located at mobile/assets/icon.png.
2026-03-14 15:07:55 -03:00

551 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<id>``/(app)/inventory/<id>` (singular, alinhado com backend)
- O scanner (`scanner.tsx`) faz match via `/meowspool:\/\/filament\/([^/]+)/` e navega para `/(app)/inventory/<id>`
- O QR Code de cada filamento exibe `meowspool://filament/<id>` 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);
}
};
<FlatList
refreshControl={<RefreshControl refreshing={isRefreshing} onRefresh={onRefresh} />}
// ... 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:
<View style={styles.logoContainer}>
<CatIcon size={48} color={colors.accent} />
</View>
// Depois:
<CatIcon size={48} color="#1E1B18" withBackground backgroundColor={colors.accent} />
```
- **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.