- Update RootLayout to handle authentication state and redirect users based on their auth status. - Create an Index component to redirect unauthenticated users to the login page. - Modify ApiAuthRepository to fetch user session data using access and refresh tokens. - Introduce a new container file to manage use case instances for authentication and filament operations. - Enhance authStore to validate tokens and fetch user data from the API. - Add babel-plugin-module-resolver for improved module imports. - Update package.json scripts for running the app on Android and iOS. - Add expo-camera dependency for camera functionalities. - Update tsconfig.json to include infrastructure path mapping.
18 KiB
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>usandoreact-native-qrcode-svg
Estratégia Offline-First
- Escrita: toda mutação persiste primeiro no SQLite (
synced = 0) e enfileira emsync_queue. - Leitura: lê sempre do SQLite; a API é usada apenas para sync.
- Sync: background job consome
sync_queuee envia para o backend Rust. - Conflito: last-write-wins via
updatedAt(ISO 8601). O backend é a fonte de verdade em conflitos. - Auth offline: token armazenado no
expo-secure-store; refresh automático via interceptor Axios.
Schema SQLite
-- 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 comouseXxxStore
Imports
Sempre usar aliases em vez de caminhos relativos:
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
SafeAreaViewcombackgroundColor: colors.bgBase - Header manual (não usa o header do expo-router):
headerShown: false - CTA fixo:
Viewcompadding: spacing[5]+borderTopWidth: 1+borderTopColor: colors.border - ScrollViews com
showsVerticalScrollIndicator={false}ekeyboardShouldPersistTaps="handled"
Formulários
react-hook-form+zodResolverpara todos os formulários- Validações no schema Zod (nunca inline)
isLoadinglocal 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_queueSQLite → API - Integrar
react-native-qrcode-svgpara render real do QR Code - Scanner de QR Code com
expo-camerav17 (ML Kit) →scanner.tsx - Gerar SVG de etiqueta (integração com
/filaments/:id/labeldo backend) - Expo Notifications para alertas de estoque baixo
- Google OAuth com
expo-auth-session - Testes de integração com Jest + Testing Library
Executando o Projeto
# 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/.envcom:EXPO_PUBLIC_API_URL=http://localhost:3000/api/v1
Atenção ao build Android: o projeto usa
namespace "com.meowspool"nobuild.gradlemas o código fonte fica emcom.meowspool.app. Por issoMainActivity.kteMainApplication.ktimportam explicitamentecom.meowspool.Recom.meowspool.BuildConfig, e oAndroidManifest.xmlusa nomes totalmente qualificados (com.meowspool.app.MainApplication,com.meowspool.app.MainActivity). Não usar nomes relativos (.MainApplication) no manifest — eles resolvem paracom.meowspool.*(sem.app), causandoClassNotFoundExceptionem runtime.