- Added support for Google OAuth with separate client IDs for Android and iOS. - Updated `verify_google_id_token` to validate `aud` against both client IDs and check `email_verified`. - Modified `google_oauth_handler` to accept and process the new client IDs. - Enhanced security by enforcing explicit JWT algorithm validation. - Updated mobile app to handle Google OAuth flow using `expo-auth-session`. - Fixed API request to send `id_token` in snake_case as expected by the backend. - Added necessary environment variables for Google client IDs in mobile app. - Implemented intent filter for Google OAuth redirect in AndroidManifest.xml.
1015 lines
46 KiB
Markdown
1015 lines
46 KiB
Markdown
# 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 (iOS) |
|
||
| Abrir arquivo | `expo-intent-launcher` | ACTION_VIEW no Android — abre apps de impressão (Niimbot) |
|
||
|
||
---
|
||
|
||
## 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/<id>` → `/(app)/inventory/<id>`
|
||
- Handler: `app/filament/[id].tsx` com `<Redirect>` do expo-router
|
||
- Mesmo link usado em QR Code, NFC e links compartilhados
|
||
- **Verificação de e-mail**: `meowspool://verify-email?token=<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=<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/<id>`
|
||
- As tags NFC NTAG215 gravam a URI `meowspool://filament/<id>` 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
|
||
- [x] Google OAuth com `expo-auth-session` (Android + iOS, `Google.useAuthRequest` + `promptAsync`)
|
||
- [ ] Testes de integração com Jest + Testing Library
|
||
|
||
---
|
||
|
||
## Mudanças Recentes (19/03/2026) — segunda entrada
|
||
|
||
### ✅ Google OAuth — implementação completa
|
||
|
||
**Arquivos modificados**: `app/(auth)/login.tsx`, `src/infrastructure/container.ts`, `src/adapters/remote/ApiAuthRepository.ts`
|
||
|
||
**Pacotes instalados**: `expo-auth-session`, `expo-web-browser`
|
||
|
||
#### Fluxo implementado
|
||
|
||
```
|
||
Usuário toca "Entrar com Google"
|
||
→ promptAsync() abre browser via expo-web-browser
|
||
→ Google OAuth consent screen
|
||
→ redirect de volta ao app (scheme meowspool://)
|
||
→ useEffect detecta response.type === 'success'
|
||
→ authentication.idToken enviado para GoogleLoginUseCase
|
||
→ POST /auth/oauth/google { id_token }
|
||
→ backend valida aud, email_verified, cria/encontra usuário
|
||
→ retorna JWT pair → setSession → navega para home
|
||
```
|
||
|
||
#### Detalhes técnicos
|
||
|
||
**`login.tsx`** — hook `Google.useAuthRequest` + `useEffect` no `response`:
|
||
|
||
```tsx
|
||
WebBrowser.maybeCompleteAuthSession(); // fora do componente
|
||
|
||
const [_request, response, promptAsync] = Google.useAuthRequest({
|
||
androidClientId: process.env.EXPO_PUBLIC_GOOGLE_CLIENT_ID_ANDROID,
|
||
iosClientId: process.env.EXPO_PUBLIC_GOOGLE_CLIENT_ID_IOS,
|
||
});
|
||
|
||
useEffect(() => {
|
||
if (response?.type === 'success') {
|
||
const idToken = response.authentication?.idToken;
|
||
googleLoginUseCase.execute({ idToken })
|
||
.then(session => setSession(session))
|
||
.then(() => router.replace('/(app)/(tabs)/home'))
|
||
.finally(() => setIsGoogleLoading(false));
|
||
}
|
||
}, [response]);
|
||
```
|
||
|
||
- `onGoogleLogin` agora é síncrono e apenas chama `promptAsync()`
|
||
- Tratamento dos três estados: `success`, `error`, `dismiss`
|
||
|
||
**`ApiAuthRepository.ts`** — bug corrigido: o campo era enviado como `idToken` (camelCase) mas o backend espera `id_token` (snake_case):
|
||
|
||
```ts
|
||
// Antes (quebrado):
|
||
await httpClient.post('/auth/oauth/google', input); // enviava { idToken }
|
||
|
||
// Depois (correto):
|
||
await httpClient.post('/auth/oauth/google', { id_token: input.idToken });
|
||
```
|
||
|
||
**`container.ts`** — adicionado export de `googleLoginUseCase`:
|
||
|
||
```ts
|
||
import { GoogleLoginUseCase } from '@application/auth/AuthUseCases';
|
||
export const googleLoginUseCase = new GoogleLoginUseCase(authRepository);
|
||
```
|
||
|
||
#### Variáveis de ambiente necessárias (`mobile/.env`)
|
||
|
||
```
|
||
EXPO_PUBLIC_GOOGLE_CLIENT_ID_ANDROID=724520558909-bg5a7e4u24jmis8lgg41ucs0kv0nfp1v.apps.googleusercontent.com
|
||
EXPO_PUBLIC_GOOGLE_CLIENT_ID_IOS=724520558909-v3kubsvmf3vda7fep8qaabenmdap53hs.apps.googleusercontent.com
|
||
```
|
||
|
||
#### Configuração no Google Cloud Console (obrigatória)
|
||
|
||
O redirect URI do `expo-auth-session` usa o scheme do app. Adicionar nos OAuth 2.0 Clients:
|
||
|
||
- **Android** (client `GOOGLE_CLIENT_ID_ANDROID`): SHA-1 fingerprint da keystore + package name `com.meowspool.app`
|
||
- **iOS** (client `GOOGLE_CLIENT_ID_IOS`): bundle ID `com.meowspool.app`
|
||
|
||
> **Nota**: o `expo-auth-session` com `Google.useAuthRequest` funciona em development build (não no Expo Go). Requer `mise exec -- npx expo run:android` após instalar os pacotes.
|
||
|
||
---
|
||
|
||
## Mudanças Recentes (19/03/2026)
|
||
|
||
### ✅ Hardening de segurança — bypass do interceptor de refresh
|
||
|
||
**Arquivo modificado**: `src/store/authStore.ts`
|
||
|
||
**Problema**: `loadStoredSession` usava `axios.get` diretamente com o access token lido do SecureStore, bypassando o interceptor de refresh do `httpClient`. Consequência: se o access token expirava, a sessão era destruída mesmo quando o refresh token ainda era válido, forçando login desnecessário.
|
||
|
||
**Correção**:
|
||
|
||
```ts
|
||
// Antes (bypass do interceptor):
|
||
const { data } = await axios.get(`${API_BASE_URL}/users/me`, {
|
||
headers: { Authorization: `Bearer ${accessToken}` },
|
||
});
|
||
|
||
// Depois (usa httpClient → interceptor de refresh ativo):
|
||
const { data } = await httpClient.get('/users/me');
|
||
// Lê tokens após possível refresh (interceptor pode ter renovado o SecureStore)
|
||
const [accessToken, refreshToken] = await Promise.all([
|
||
SecureStore.getItemAsync(SECURE_STORE_ACCESS_TOKEN),
|
||
SecureStore.getItemAsync(SECURE_STORE_REFRESH_TOKEN),
|
||
]);
|
||
```
|
||
|
||
**Melhorias adicionais no `catch`**:
|
||
|
||
```ts
|
||
const isAuthError =
|
||
axios.isAxiosError(err) &&
|
||
(err.response?.status === 401 || err.response?.status === 403);
|
||
if (isAuthError || !axios.isAxiosError(err)) {
|
||
await SecureStore.deleteItemAsync(SECURE_STORE_ACCESS_TOKEN);
|
||
await SecureStore.deleteItemAsync(SECURE_STORE_REFRESH_TOKEN);
|
||
}
|
||
set({ isLoading: false });
|
||
```
|
||
|
||
- Erros de rede (sem conexão) não forçam logout — tokens são preservados para quando a conectividade voltar
|
||
- Somente erros 401/403 limpam a sessão
|
||
- Import `API_BASE_URL` removido (não mais necessário)
|
||
- Import `httpClient` adicionado de `@adapters/remote/httpClient`
|
||
|
||
---
|
||
|
||
## 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=<token>` após o registro
|
||
2. Usuário toca no link → browser abre → backend redireciona 302 → `meowspool://verify-email?token=<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<void> {
|
||
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 <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) → Android: `IntentLauncher.startActivityAsync('android.intent.action.VIEW', ...)` com `content://` URI via `FileSystem.getContentUriAsync` | iOS: `Sharing.shareAsync`
|
||
- **Fluxo SVG**: `GET /label.svg` → resposta como texto → salvar com encoding `'utf8'` → mesmo padrão Android/iOS acima
|
||
- **Por que ACTION_VIEW no Android**: `expo-sharing` usa `ACTION_SEND`, que não é declarado pelo Niimbot (e outros apps de impressão). Esses apps declaram apenas `ACTION_VIEW`. Usar `IntentLauncher` com `ACTION_VIEW` + `content://` URI mostra o mesmo seletor que o Gmail usa ao abrir um PDF.
|
||
- **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.
|