Files
MeowSpool/mobile/agent.md
T
2026-03-14 19:45:08 -03:00

27 KiB
Raw Blame History

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 ~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
Câmera / Scanner expo-camera ~17 ML Kit barcode scan; requer development build
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)
├── 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 (PDF ou 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
  • 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

-- 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:

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)
GET /filaments/:id/label.pdf Etiqueta PDF (width_mm, height_mm, fields)

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
  • Integrar react-native-qrcode-svg para render real do QR Code
  • Scanner de QR Code com expo-camera v17 (ML Kit) → scanner.tsx
  • Exportar etiqueta SVG via GET /filaments/:id/label.svg
  • Exportar etiqueta PDF via GET /filaments/:id/label.pdf
  • Seletor de formato (PDF/SVG) e controle de campos na tela de etiqueta
  • 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)

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 arraybufferbtoa 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:
    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:

    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:

    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:

    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: deleteFilamentUseCaseremoveFilament()
    • Update: updateFilamentUseCaseupdateFilament(result)
    • Refresh: listFilamentsUseCasesetFilaments(result)
    • Create: createFilamentUseCaseaddFilament(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:

    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):

    // 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:

    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

# 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.