Files
MeowSpool/mobile/agent.md
T

772 lines
35 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 ~54), 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`.
> **Bare Workflow**: o projeto usa `expo prebuild` (diretório `android/` comitado). Qualquer novo pacote com módulo nativo exige `npx expo prebuild --platform android` + rebuild completo do APK.
---
## Stack
| Componente | Biblioteca | Observação |
| ----------------- | ----------------------------------- | --------------------------------------------------------- |
| Framework | `expo` ~54 | Managed Workflow |
| Navigation | `expo-router` ~4 | 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 |
| SVG inline | `react-native-svg` | SvgXml para renderizar SVG como string; sem transformer |
| Câmera / Scanner | `expo-camera` ~17 | ML Kit barcode scan; requer development build |
| NFC | `react-native-nfc-manager` | Leitura e gravação NDEF (NTAG215); requer build nativo |
| File system | `expo-file-system/legacy` | Salvar arquivos no cache; usar import `/legacy` |
| Compartilhamento | `expo-sharing` | Sheet nativo de compartilhamento de arquivos |
---
## 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)
│ └── nfc/ ← Implementação NFC (react-native-nfc-manager)
├── 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
│ ├── filament/
│ │ └── [id].tsx ← Deep link handler: redireciona meowspool://filament/{id} → /(app)/inventory/{id}
│ └── (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)
│ ├── nfc-reader.tsx ← 1RY-0 Bottom sheet modal de leitura NFC
│ └── filaments/
│ └── [id]/
│ ├── qrcode.tsx ← Ver QR Code (react-native-qrcode-svg)
│ ├── label.tsx ← Exportar Etiqueta (PDF ou SVG)
│ └── write-nfc.tsx ← Gravar tag NTAG215 com deep link do filamento
└── 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
│ └── INFCRepository.ts ← interface INFCRepository (isSupported, readTag, writeTag, cancelSession)
├── 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.
│ └── nfc/
│ ├── ReadNFCTagUseCase.ts ← lê URI, valida schema meowspool://, extrai filament ID
│ └── WriteNFCTagUseCase.ts ← monta meowspool://filament/{id} e grava na tag
├── 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
│ └── nfc/
│ └── NFCManagerRepository.ts ← implementa INFCRepository via react-native-nfc-manager
├── 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 redirecionamento é feito pela rota `app/filament/[id].tsx` com `<Redirect>` do expo-router — garante que o deep link abrido externamente (QR Code, NFC, link compartilhado) sempre chegue na tela correta
- 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`
- As tags NFC NTAG215 gravam a mesma URI `meowspool://filament/<id>` como NDEF URI record — mesmo deep link, infraestrutura compartilhada
---
## 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 PNG |
| GET | `/filaments/:id/label.svg` | Etiqueta SVG (`width_mm`, `height_mm`, `fields`, `dpi`) |
| GET | `/filaments/:id/label.pdf` | Etiqueta PDF (`width_mm`, `height_mm`, `fields`, `dpi`) |
### 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`
- [x] Exportar etiqueta SVG via `GET /filaments/:id/label.svg`
- [x] Exportar etiqueta PDF via `GET /filaments/:id/label.pdf`
- [x] Seletor de formato (PDF/SVG) e controle de campos na tela de etiqueta
- [x] Suporte a Niimbot D11/D110 (22×14mm) com seleção automática de conteúdo mínimo
- [x] NFC leitura NTAG215 → `nfc-reader.tsx` (bottom sheet modal)
- [x] NFC gravação NTAG215 → `filaments/[id]/write-nfc.tsx`
- [ ] NFC suporte iOS (requer entitlement `com.apple.developer.nfc.readwrite`)
- [ ] 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)
### ✅ Deep Link Handler — `app/filament/[id].tsx`
Criada rota `app/filament/[id].tsx` para tratar deep links abertos externamente (QR Code, NFC, mensagem compartilhada). Sem essa rota, o expo-router exibia "Unmatched Route" ao abrir `meowspool://filament/{id}` fora do app.
```tsx
import { Redirect, useLocalSearchParams } from 'expo-router';
export default function FilamentDeepLink() {
const { id } = useLocalSearchParams<{ id: string }>();
return <Redirect href={`/(app)/inventory/${id}` as never} />;
}
```
O redirecionamento leva para `/(app)/inventory/[id]` (tela de detalhe), que já lida com usuário não autenticado via o guard do `_layout.tsx` raiz.
---
### ✅ Suporte a Impressoras Pequenas — Niimbot D11/D110 (`label.tsx`)
**Arquivo alterado**: `app/(app)/filaments/[id]/label.tsx`
#### Preset Niimbot 22×14mm
Adicionado como primeira opção em `LABEL_SIZES`:
```ts
{ id: '22x14', label: '22 × 14', sub: 'Niimbot D11/D110' }
```
O tipo `LabelSize` foi atualizado para incluir `'22x14'`.
#### Seleção automática de conteúdo mínimo
A função `handleSizeChange` substitui o `setSelectedSize` direto nos chips. Ao selecionar `22x14`, o conteúdo ativo é automaticamente reduzido para `{ color, name, qrcode }` — os únicos campos que cabem fisicamente em 22×14mm. O usuário pode ajustar manualmente após.
```ts
const MINI_LABEL_CONTENT: Set<ContentOption> = new Set(['color', 'name', 'qrcode']);
function handleSizeChange(size: LabelSize): void {
setSelectedSize(size);
if (size === '22x14') setEnabledContent(new Set(MINI_LABEL_CONTENT));
}
```
#### `dpi=203` enviado para o backend
O parâmetro `dpi` é incluído automaticamente nos requests de `/label.pdf` e `/label.svg` quando Niimbot está selecionado. Instrui o backend a calcular pixels do SVG na resolução correta da impressora térmica.
#### Fix: btoa chunking para PDFs grandes
O código original usava `String.fromCharCode(...new Uint8Array(buffer))` com spread, que causa **stack overflow** em buffers > ~64KB em React Native. Substituído por loop em chunks de 8192 bytes:
```ts
const bytes = new Uint8Array(response.data);
let binary = '';
const CHUNK = 8192;
for (let i = 0; i < bytes.length; i += CHUNK) {
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
}
const base64 = btoa(binary);
```
Essa correção se aplica a todos os tamanhos de etiqueta, não apenas Niimbot.
---
### ✅ NFC Read/Write — NTAG215
Implementação completa de leitura e gravação NFC para tags NTAG215. O conteúdo gravado é idêntico ao QR Code: `meowspool://filament/{id}` como NDEF URI record.
**Pacote instalado**: `react-native-nfc-manager`
**Arquitetura (mesma camada hexagonal do projeto)**:
| Camada | Arquivo | Responsabilidade |
|--------|---------|-----------------|
| Port | `src/ports/INFCRepository.ts` | Interface: `isSupported`, `readTag`, `writeTag`, `cancelSession` |
| Adapter | `src/adapters/nfc/NFCManagerRepository.ts` | Implementação via `react-native-nfc-manager` |
| Use Case | `src/application/nfc/ReadNFCTagUseCase.ts` | Lê URI, valida schema `meowspool://filament/`, extrai ID |
| Use Case | `src/application/nfc/WriteNFCTagUseCase.ts` | Monta `meowspool://filament/{id}` e delega ao adapter |
| DI | `src/infrastructure/container.ts` | Exporta `nfcRepository`, `readNFCTagUseCase`, `writeNFCTagUseCase` |
**Telas**:
- **`app/(app)/nfc-reader.tsx`** (artboard `1RY-0`): bottom sheet modal com animação de ondas pulsantes (3 anéis `Animated`), badge "LENDO...", título/subtítulo e botão Cancelar. Apresentado como `presentation: 'transparentModal'` via `_layout.tsx`. Ao detectar a tag navega diretamente para `/(app)/inventory/<id>`.
- **`app/(app)/filaments/[id]/write-nfc.tsx`**: tela de gravação com 3 estados visuais (escrevendo / sucesso / erro), mesma animação de ondas. Botão "Gravar NFC" aparece no card de Identificação do Detalhe do Filamento, ao lado de "Ver QR".
**Ponto de entrada — Home (`home.tsx`)**: botão NFC adicionado à esquerda do botão QR no header. Checa `isSupported()` antes de abrir o modal; exibe `Alert` se NFC não estiver disponível.
**Ícone NFC**: renderizado via `SvgXml` do `react-native-svg` (sem SVG transformer). O template string com os 4 arcos SVG é definido no topo de cada arquivo que usa o ícone — não há wrapper de componente.
**Inicialização do módulo nativo**:
`NfcManager.start()` deve ser chamado antes de `requestTechnology`. O adapter usa uma flag `started` e chama `start()` de forma lazy (apenas em `readTag`/`writeTag`). **`isSupported()` não chama `start()`** — faz apenas um check do módulo nativo via `NativeModules.NfcManager != null` + try/catch.
```ts
// Padrão correto:
async isSupported(): Promise<boolean> {
if (!this.nativeModuleAvailable) return false;
try { return await NfcManager.isSupported(); } catch { return false; }
}
// start() só é chamado quando vai usar NFC de verdade:
private async ensureStarted(): Promise<void> {
if (!this.nativeModuleAvailable) throw new Error('Módulo NFC não disponível.');
if (!this.started) { await NfcManager.start(); this.started = true; }
}
```
**⚠️ Requer build nativo**: `react-native-nfc-manager` usa módulo nativo. Não funciona no Expo Go. Após instalar ou rodar `expo prebuild`, é obrigatório recompilar o APK:
```bash
mise exec -- npx expo run:android
```
**Permissões Android** (adicionadas automaticamente pelo plugin no `app.json`):
```xml
<uses-permission android:name="android.permission.NFC"/>
```
**iOS**: não implementado nesta fase. Requer entitlement `com.apple.developer.nfc.readwrite` da Apple Developer Program. A arquitetura suporta adição futura sem mudanças nas camadas acima do adapter.
---
### ✅ Exportação de Etiqueta PDF e SVG (`label.tsx`)
- **Pacotes instalados**: `expo-file-system`, `expo-sharing`, `react-native-worklets`
- **Atenção**: importar `expo-file-system` como `expo-file-system/legacy` — a API padrão do SDK 54 não exporta `EncodingType` nem `writeAsStringAsync` diretamente
- **Fluxo PDF**: `GET /label.pdf` → resposta como `arraybuffer``btoa` para Base64 → salvar com `FileSystem.writeAsStringAsync` (encoding Base64) → `Sharing.shareAsync`
- **Fluxo SVG**: `GET /label.svg` → resposta como texto → salvar com encoding `'utf8'``Sharing.shareAsync`
- **Seletor de formato**: chips PDF/SVG no footer; padrão é PDF
- **Campos selecionáveis**: `enabledContent` (Set) é convertido para string CSV e enviado como `fields` query param — o backend respeita a seleção
### ✅ 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
#### 7. **Color Picker — Modal Customizada (Substituindo Alert.prompt)**
- **Arquivos**: `app/(app)/inventory/new.tsx`, `app/(app)/inventory/[id]/edit.tsx`
- **Problema**: `Alert.prompt()` apresentava bugs e comportamentos inconsistentes em React Native/Expo
- **Solução**: Implementada Modal customizada com componentes nativos
- **Mudanças**:
- Adicionadas importações: `Modal`, `KeyboardAvoidingView`, `Platform` do React Native
- Novos estados: `isColorPickerOpen` (boolean), `colorInputValue` (string)
- Funções de controle:
- `handleOpenColorPicker()`: abre modal focando no input
- `handleConfirmColor()`: valida hex, atualiza cor, fecha modal
- `handleCancelColor()`: descarta input, fecha modal
- JSX: Modal envolta em Fragment com overlay semi-transparente
- Estilos novos: `modalOverlay`, `modalContent`, `modalCard`, `modalInput`, `modalButtons`, etc.
- **Código de exemplo**:
```typescript
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
const [colorInputValue, setColorInputValue] = useState('');
const handleConfirmColor = (): void => {
if (!colorInputValue) {
Alert.alert('Erro', 'Digite um código hexadecimal');
return;
}
const normalized = normalizeHex(colorInputValue.trim());
if (isValidHex(normalized)) {
setSelectedColor(normalized);
setHexInput(normalized);
setIsColorPickerOpen(false);
setColorInputValue('');
} else {
Alert.alert('Erro', 'Código hexadecimal inválido. Use o formato #RRGGBB ou #RGB.');
}
};
// No JSX:
<Modal visible={isColorPickerOpen} transparent animationType="fade">
<View style={styles.modalOverlay}>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
<View style={styles.modalCard}>
<TextInput
placeholder="#FF5733"
value={colorInputValue}
onChangeText={setColorInputValue}
autoFocus
/>
{/* Botões confirmar/cancelar */}
</View>
</KeyboardAvoidingView>
</View>
</Modal>
```
- **Impacto**:
- ✅ Color picker agora funciona consistentemente em todas plataformas
- ✅ UX melhorada com feedback visual imediato
- ✅ Suporte completo a teclado (autoFocus, KeyboardAvoidingView)
- ✅ Validação de hex pré-digitação com `isValidHex()` e `normalizeHex()`
### 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.