# 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 │ │ ├── check-email.tsx ← RU-0 Tela informativa pós-cadastro ("Confirme seu e-mail") │ │ ├── reset-password.tsx ← 117-0 │ │ └── password-reset-done.tsx ← 135-0 │ ├── filament/ │ │ └── [id].tsx ← Deep link handler: redireciona meowspool://filament/{id} → /(app)/inventory/{id} │ ├── verify-email.tsx ← Deep link handler: meowspool://verify-email?token=xxx → chama API e exibe resultado │ └── (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, ForgotPassword, ResendVerification, VerifyEmail, ResetPassword │ └── 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://` (configurado em `app.json` como `scheme: "meowspool"`) - **Filamento**: `meowspool://filament/` → `/(app)/inventory/` - Handler: `app/filament/[id].tsx` com `` do expo-router - Mesmo link usado em QR Code, NFC e links compartilhados - **Verificação de e-mail**: `meowspool://verify-email?token=` - Handler: `app/verify-email.tsx` — chama `VerifyEmailUseCase.execute(token)` automaticamente - Exibe estados: carregando → sucesso → erro (token inválido/expirado) - Redireciona para `/(auth)/login` ao concluir - **Reset de senha**: `meowspool://reset-password?token=` - Handler: `app/(auth)/reset-password.tsx` — lê `token` via `useLocalSearchParams` - O scanner (`scanner.tsx`) faz match via `/meowspool:\/\/filament\/([^/]+)/` e navega para `/(app)/inventory/` - As tags NFC NTAG215 gravam a URI `meowspool://filament/` como NDEF URI record > **Links de e-mail**: o backend gera links `https://meowspool.felipecncloud.com/api/v1/verify-email?token=xxx` (aceitos por clientes de e-mail). O backend redireciona (302) para `meowspool://verify-email?token=xxx`. O OS reconhece o scheme e abre o app. > - `APP_BASE_URL=https://meowspool.felipecncloud.com/api/v1` > - `APP_SCHEME=meowspool` --- ## 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/resend-verification` | Reenviar e-mail de verificação | | 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 - [x] Deep link `verify-email?token=` com handler e integração ao backend - [ ] Google OAuth com `expo-auth-session` - [ ] Testes de integração com Jest + Testing Library --- ## Mudanças Recentes (14/03/2026) ### ✅ Reenvio de e-mail de verificação **Arquivos modificados**: `src/ports/AuthRepository.ts`, `src/adapters/remote/ApiAuthRepository.ts`, `src/application/auth/AuthUseCases.ts`, `src/infrastructure/container.ts`, `app/(auth)/check-email.tsx`, `app/(auth)/register.tsx`, `app/(auth)/login.tsx` **Fluxo**: - `register.tsx` e `login.tsx` passam o email como query param: `/(auth)/check-email?email=xxx` - `check-email.tsx` lê `email` via `useLocalSearchParams` e exibe botão "Reenviar e-mail" - Cooldown de 60s após cada envio para evitar spam - Feedback visual: "E-mail reenviado! Verifique sua caixa de entrada." - Botão só aparece se `email` estiver disponível nos params **Cadeia adicionada**: - `AuthRepository.resendVerification(email)` — novo método no port - `ApiAuthRepository.resendVerification` — `POST /auth/resend-verification` - `ResendVerificationUseCase` — valida email não vazio e delega ao repo - `resendVerificationUseCase` exportado do `container.ts` --- ### ✅ Verificação de e-mail obrigatória antes do login **Arquivos modificados**: `src/ports/AuthRepository.ts`, `src/adapters/remote/ApiAuthRepository.ts`, `src/application/auth/AuthUseCases.ts`, `app/(auth)/register.tsx`, `app/(auth)/login.tsx`, `app/_layout.tsx` **Renomeação**: `app/(auth)/verify-email.tsx` → `app/(auth)/check-email.tsx` - **Motivo**: no expo-router, grupos `(auth)` são transparentes na URL. `app/(auth)/verify-email.tsx` e `app/verify-email.tsx` competiam pelo mesmo path `/verify-email`, fazendo o deep link cair na tela informativa em vez do handler de verificação. **Mudanças**: - `RegisterUseCase.execute` / `ApiAuthRepository.register` / `AuthRepository.register` — retornam `void` (backend não emite mais tokens no registro) - `register.tsx` — remove `setSession`, navega para `/(auth)/check-email` após cadastro (usuário fica não autenticado) - `login.tsx` — detecta `EMAIL_NOT_VERIFIED` (403 do backend) e exibe alerta com link para `/(auth)/check-email`: ```ts if (axios.isAxiosError(err) && err.response?.data?.code === 'EMAIL_NOT_VERIFIED') { Alert.alert('E-mail não verificado', '...', [ { text: 'OK', onPress: () => router.push('/(auth)/check-email') }, ]); } ``` - `app/_layout.tsx` — auth guard agora exclui rotas públicas da raiz do redirect para login: ```ts const inPublicRoute = segments[0] === 'verify-email' || segments[0] === 'filament'; // ... } else if (!isAuthenticated && !inAuthGroup && !inPublicRoute) { router.replace('/(auth)/login'); } ``` **Sem esse fix**, o guard redirecionava `verify-email` para login antes de o usuário ver a tela de sucesso. **Rotas públicas da raiz** (não sofrem redirect do auth guard): | Rota | Arquivo | Função | |---|---|---| | `/verify-email` | `app/verify-email.tsx` | Handler deep link — verifica e-mail e exibe sucesso/erro | | `/filament` | `app/filament/[id].tsx` | Handler deep link — redireciona para detalhe do filamento | --- ### ✅ Redirect HTTP → Deep Link (fix compatibilidade com clientes de e-mail) **Contexto**: clientes de e-mail (Gmail) bloqueiam links com scheme customizado (`meowspool://`). O link não abria o app. **Solução implementada no backend**: o e-mail agora contém link `https://`. O backend (GET `/api/v1/verify-email?token=xxx`) redireciona 302 → `meowspool://verify-email?token=xxx`. O OS reconhece o scheme e abre o app normalmente. **Impacto no mobile**: nenhuma alteração necessária no app — `app/verify-email.tsx` continua recebendo o deep link e chamando o backend via POST como antes. --- ### ✅ Deep Link de Verificação de E-mail — `app/verify-email.tsx` **Arquivos criados/modificados**: `app/verify-email.tsx`, `app/_layout.tsx`, `src/application/auth/AuthUseCases.ts`, `src/infrastructure/container.ts` #### Fluxo 1. Backend envia e-mail com link `https://meowspool.felipecncloud.com/api/v1/verify-email?token=` após o registro 2. Usuário toca no link → browser abre → backend redireciona 302 → `meowspool://verify-email?token=` 3. OS reconhece o scheme → expo-router roteia para `app/verify-email.tsx` 4. A tela lê `token` via `useLocalSearchParams` e chama `VerifyEmailUseCase.execute(token)` em `useEffect` 5. Exibe estado **loading** durante a chamada, **sucesso** ou **erro** (token inválido / expirado / já usado) 6. Botão redireciona para `/(auth)/login` #### `VerifyEmailUseCase` Adicionado em `src/application/auth/AuthUseCases.ts`: ```ts export class VerifyEmailUseCase { constructor(private readonly authRepo: AuthRepository) {} async execute(token: string): Promise { if (!token) throw new Error('Token inválido.'); await this.authRepo.verifyEmail(token); } } ``` - `ApiAuthRepository.verifyEmail(token)` já existia (`POST /auth/verify-email` com `{ token }`) - `verifyEmailUseCase` exportado do `container.ts` - Rota `verify-email` registrada no `Stack` do `app/_layout.tsx` --- ### ✅ 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 ; } ``` 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 = 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/`. - **`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 { 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 { 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 ``` **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); } }; } // ... 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 #### 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: {/* Botões confirmar/cancelar */} ``` - **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.