diff --git a/pedi-foods/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml similarity index 100% rename from pedi-foods/.gitea/workflows/ci.yml rename to .gitea/workflows/ci.yml diff --git a/pedi-foods/.gitea/workflows/pr-validation.yml b/.gitea/workflows/pr-validation.yml similarity index 100% rename from pedi-foods/.gitea/workflows/pr-validation.yml rename to .gitea/workflows/pr-validation.yml diff --git a/pedi-foods/.gitignore b/.gitignore similarity index 100% rename from pedi-foods/.gitignore rename to .gitignore diff --git a/API_Mobile_App.md b/API_Mobile_App.md deleted file mode 100644 index d410e8f..0000000 --- a/API_Mobile_App.md +++ /dev/null @@ -1,1288 +0,0 @@ -# Atomenta Mobile App API Documentation - -## 🔐 Authentication & Headers - -The mobile app accesses distinct sets of APIs: -1. **App APIs (`/api/app/*`)**: For general app features like Home Screen and Orders listing. -2. **Customer APIs (`/api/customer/*`)**: For User Registration, Login, and Profile Management. -3. **Store External APIs (`/api/store/*`)**: For store-specific operations like Catalog and Checkout. - -### Common Headers -All requests should ideally include: - -```http -Accept: application/json -Content-Type: application/json -``` - -### Authentication Strategies - -#### 1. Customer Auth (for App & Customer APIs) -Used for everything related to the logged-in user (Profile, Orders, etc). -- **Header:** `Authorization: Bearer ` -- **Note:** Obtained via `/api/customer/login`. - -#### 3. Hybrid Store Auth (for Store External APIs) -Used when interacting with a specific store (Catalog, Checkout). -- **Header 1:** `Atomenta-Token: 550e8400-e29b-41d4-a716-446655440008` (Store Module ID) -- **Header 2:** `Authorization: Bearer ` (Required for Checkout/Orders) - ---- - -## 👤 Customer Management - -### 1. Register Customer -**POST** `/api/customer` - -**Headers:** -- `Atomenta-Token: 550e8400-e29b-41d4-a716-44665544000a` (Customer Module ID) - -**Body:** - -```json -{ - "name": "Daniel Loverde", - "email": "daniel@example.com", - "phoneNumber": "+5511999999999", - "birthDate": "1990-01-01T00:00:00Z" // Opcional -} -``` - -**Response:** - -```json -{ - "error": false, - "code": "CUSTOMER_CREATED", - "result": { - "id": "cust_uuid...", - "name": "Daniel Loverde", - "email": "daniel@example.com" - } -} -``` - -### 2. Login (Get Token) -**POST** `/api/customer/login` - -**Body (email + phoneNumber are mandatory):** - -```json -{ - "email": "daniel@example.com", - "phoneNumber": "+5511999999999" -} -``` - -**Fluxo:** -- Passo 1: enviar `email` + `phoneNumber` para receber código por email. -- Passo 2: enviar `email` + `phoneNumber` e o OTP no header `Authorization: Bearer `. - -**Response:** - -```json -{ - "error": false, - "code": "LOGIN_SUCCESS", - "result": { - "token": "eyJhbGciOi...", // <--- Use as Bearer Token for other requests - "customer": { - "id": "cust_uuid...", - "name": "Daniel Loverde", - "email": "daniel@example.com" - } - } -} -``` - -### 3. Get Profile -**GET** `/api/customer/profile` - -**Headers:** - -- `Authorization: Bearer ` -- `Atomenta-Token: 550e8400-e29b-41d4-a716-44665544000a` - -**Response:** - -```json -{ - "error": false, - "code": "CUSTOMER_PROFILE_RETRIEVED", - "result": { - "id": "cust_uuid...", - "name": "Daniel Loverde", - "email": "daniel@example.com", - "phoneNumber": "+5511999999999", - "favorites": ["store_abc...", "store_xyz..."], - "address_book": [], - "verified": true, - "biometricsEnabled": false, - "createdAt": "2026-01-21T22:37:56.472Z", - "updatedAt": "2026-02-13T19:00:00.000Z" - } -} -``` - -**Campos internos não retornados para o app:** `keyHash`, `strongHash`, `source`, `isShadow`, `profile_metrics`, `behavioral_stats`, `otpHash`, `otpHashParams`, `otpExpiresAt`. - -### 4. Update Profile -**PATCH** `/api/customer/profile` - -**Headers:** - -- `Authorization: Bearer ` -- `Atomenta-Token: 550e8400-e29b-41d4-a716-44665544000a` -- `Content-Type: application/json` - -**Notes:** - -- Todos os campos são opcionais — envie apenas o que mudou. -- `profilePicture` aceita base64 data URL (máx 2 MB decodificado). -- A resposta inclui `profilePictureUrl` apenas quando imagem foi enviada. - -**Body:** - -```json -{ - "name": "Daniel A. Loverde", - "cpf": "12345678901", - "phoneNumber": "5511988888888", - "profilePicture": "data:image/jpeg;base64,/9j/4AAQSkZJRgAB..." -} -``` - -| Campo | Tipo | Obrigatório | Descrição | -|---|---|---|---| -| `name` | string | não | Nome de exibição | -| `cpf` | string | não | 11 dígitos, formatado ou raw | -| `phoneNumber` | string | não | Com DDI, sem `+`: `"5519991670000"` | -| `profilePicture` | string | não | base64 data URL — jpeg, png ou webp — máx 2 MB decoded | - -**Formato `profilePicture`:** - -``` -data:image/jpeg;base64, -data:image/png;base64, -data:image/webp;base64, -``` - -**Response (sucesso):** - -```json -{ - "error": false, - "code": "PROFILE_UPDATED", - "profilePictureUrl": "/uploads/profiles/e30188cf_1780615191653.jpg" -} -``` - -> `profilePictureUrl` é URL relativa. Prefixar com `https://atomenta.com.br` para exibir a imagem. - -**Erros possíveis:** - -| Código | HTTP | Descrição | -|---|---|---| -| `INVALID_CPF` | 400 | CPF não tem 11 dígitos | -| `INVALID_PROFILE_IMAGE` | 400 | Não é data URL válido ou excede 2 MB | -| `NO_FIELDS` | 400 | Nenhum campo reconhecido no body | -| `CUSTOMER_NOT_FOUND` | 404 | — | - -### 5. Set Default Address -**PATCH** `/api/customer/addresses/:addressId/default` - -**Headers:** - -- `Authorization: Bearer ` -- `Atomenta-Token: 550e8400-e29b-41d4-a716-44665544000a` - -**Notes:** - -- Define um endereço como padrão pelo seu `id`. -- Define `isDefault: true` no endereço alvo e `isDefault: false` em todos os outros. -- O `id` de cada endereço é retornado no campo `address_book` do perfil (`GET /api/customer/profile`). -- Ao salvar um endereço via `PATCH /api/customer/profile`, o backend gera automaticamente um `id` UUID caso o endereço não possua um. -- Se nenhum endereço tiver `isDefault: true`, o primeiro do array é tratado como padrão. - -**Response 200:** - -```json -{ - "error": false, - "code": "ADDRESS_DEFAULT_SET" -} -``` - -**Erros:** - -| Código | HTTP | Descrição | -|---|---|---| -| `ADDRESS_NOT_FOUND` | 404 | Nenhum endereço com esse `id` no `address_book` | -| `CUSTOMER_NOT_FOUND` | 404 | — | - -**Exemplo de `address_book` no perfil (com `id` e `isDefault`):** - -```json -"address_book": [ - { - "id": "a1b2c3d4-...", - "label": "Casa", - "type": "residential", - "address": "Estrada Vicinal Nene Moro", - "number": "SN", - "neighborhood": "Curtume", - "city": "Aguaí", - "state": "SP", - "zipCode": "13868070", - "country": "Brasil", - "lat_long": [-22.06743, -46.98018], - "isDefault": true - } -] -``` - ---- - -### 6. List Favorite Stores -**GET** `/api/customer/favorites` - -**Headers:** - -- `Authorization: Bearer ` -- `Atomenta-Token: 550e8400-e29b-41d4-a716-44665544000a` - -**Notes:** - -- Retorna as lojas favoritas do customer autenticado. -- Apenas lojas públicas/visíveis são retornadas. -- A ordem segue a ordem salva em `favorites`. - -**Response:** - -```json -{ - "error": false, - "code": "CUSTOMER_FAVORITES_RETRIEVED", - "result": [ - { - "id": "store_1775510226293_yn5ys", - "name": "CPS Drinks", - "logo": null, - "cover": null, - "category": "Doces & Bolos", - "rating": 4.8, - "totalReviews": 12, - "isOpen": true, - "statusLabel": "Aberto", - "nextOpenLabel": null - } - ] -} -``` - -### 6. Add Store to Favorites -**POST** `/api/customer/favorites/:storeId` - -**Headers:** - -- `Authorization: Bearer ` -- `Atomenta-Token: 550e8400-e29b-41d4-a716-44665544000a` - -**Notes:** - -- Salva a loja nos favoritos do customer autenticado. -- Se a loja já estiver favoritada, a operação continua idempotente. -- A loja precisa existir e estar publicamente visível. - -**Response:** - -```json -{ - "error": false, - "code": "CUSTOMER_FAVORITE_SAVED", - "result": { - "favorites": ["store_1775510226293_yn5ys"], - "store": { - "id": "store_1775510226293_yn5ys", - "name": "CPS Drinks", - "logo": null, - "cover": null, - "category": "Doces & Bolos", - "rating": 4.8, - "totalReviews": 12, - "isOpen": true, - "statusLabel": "Aberto", - "nextOpenLabel": null - } - } -} -``` - -### 7. Remove Store from Favorites -**DELETE** `/api/customer/favorites/:storeId` - -**Headers:** - -- `Authorization: Bearer ` -- `Atomenta-Token: 550e8400-e29b-41d4-a716-44665544000a` - -**Notes:** - -- Remove a loja dos favoritos do customer autenticado. -- A operação é idempotente: se a loja não estiver favoritada, o array volta sem ela. - -**Response:** - -```json -{ - "error": false, - "code": "CUSTOMER_FAVORITE_REMOVED", - "result": { - "favorites": [] - } -} -``` - ---- - -## 🏠 App Home Screen - -### List Categories -**GET** `/api/public/categories` - -**Headers:** -- `Accept: application/json` - -**Response:** - -```json -{ - "error": false, - "result": [ - { "id": "all", "name": "Todas", "icon": "🏪" }, - { "id": "lanches", "name": "Lanches", "icon": "🍔" }, - { "id": "pizza", "name": "Pizza", "icon": "🍕" } - ] -} -``` - -### List Stores (Smart Listing) -**GET** `/api/app/stores` - -**Headers:** -- `Authorization: Bearer ` (Customer obrigatório) - -Lists stores based on user location, filtered by distance. - -**Query Parameters:** - -- `lat` (Required): User Latitude (e.g., `-23.550520`) -- `lng` (Required): User Longitude (e.g., `-46.633308`) -- `category` (Optional): Filter by category name (e.g., `Lanches`) -- `search` (Optional): Search by store name - -**Notes:** - -- Sem login do customer, a listagem é bloqueada. -- Se `lat/lng` não forem enviados, a API tenta usar o endereço salvo no perfil do customer (`address_book.lat_long`). -- Lojas fora do raio/bairro de entrega não são exibidas. - -**Response:** - -```json -{ - "error": false, - "result": [ - { - "id": "store_123...", - "name": "Burger King", - "logo": "https://...", - "cover": "https://...", - "category": "Lanches", - "rating": 4.8, - "deliveryTime": "30-45 min", - "deliveryFee": 5.99, - "distance": 1.2, // km - "isOpen": true, - "statusLabel": "Aberto" - } - ] -} -``` - -### List User Orders -**GET** `/api/app/orders` - -**Headers:** - -- `Authorization: Bearer ` - -**Response:** - -```json -{ - "error": false, - "result": [ - { - "id": "ord_123...", - "total": 54.90, - "status": "completed", - "createdAt": "2024-03-20T10:00:00Z", - "items": [...] - } - ] -} -``` - ---- - -## 🏪 Store Integration (External API) - -**Base URL:** `/api/store/:storeId` - -### 1. Store Identity & Status -**GET** `/api/store/:storeId/identity` - -**Headers/Query:** - -- `x-user-lat` / `lat`: User Latitude -- `x-user-lng` / `lng`: User Longitude - -**Response:** - -```json -{ - "error": false, - "result": { - "fantasyName": "Mc Donalds", - "distance": 2.5, - "deliveryTime": "30-45 min", - "minOrder": 15.00 - } -} -``` - -### 2. Full Store Info -**GET** `/api/store/:storeId/info` - -Retorna versão pública para app (sem dados administrativos/financeiros da loja). - -**Response:** - -```json -{ - "error": false, - "code": "STORE_INFO_RETRIEVED", - "result": { - "isOpen": true, - "statusLabel": "Aberto", - "fantasyName": "Mc Donalds", - "specialty": "Lanches", - "minOrder": 15, - "deliveryTime": "30-45 min", - "logo": "https://...", - "cover": "https://...", - "paymentMethods": { - "paymentOnDelivery": true, - "paymentOnPickup": true, - "acceptCash": true, - "acceptPix": true - }, - "openingHours": { "...": "..." }, - "address": { - "street": "Rua X", - "neighborhood": "Centro", - "city": "Campinas", - "state": "SP", - "zipcode": "13000-000", - "latitude": -22.9, - "longitude": -47.0 - } - } -} -``` - -### 3. Product Catalog -**GET** `/api/store/:storeId/catalog` - -Retorna categorias com produtos, incluindo diferenciação explícita de pizza. - -**Response:** - -```json -{ - "error": false, - "result": [ - { - "id": "cat_lanches", - "name": "Lanches", - "isPizzaCategory": false, - "products": [ - { - "id": "prod_xburger", - "type": "prepared", - "name": "X-Burger", - "description": "Pão, carne e queijo", - "image": "/uploads/products/xburger.jpg", - "price": "25.00", - "addons": "yes", - "addonGroups": [] - } - ] - }, - { - "id": "cat_pizzas", - "name": "Pizzas", - "isPizzaCategory": true, - "pizzaConfig": { - "sizes": [{ "id": "broto", "name": "Broto", "slices": 4, "maxFlavors": 1 }], - "doughs": [{ "id": "tradicional", "name": "Tradicional", "active": true }], - "crusts": [{ "id": "catupiry", "name": "Catupiry", "active": true, "priceModifier": "10.00" }] - }, - "products": [ - { - "id": "prod_pizza_calabresa", - "type": "pizza", - "name": "Pizza Calabresa", - "description": "Molho, mussarela e calabresa", - "image": "/uploads/products/pizza-calabresa.jpg", - "price": "0.00", - "pizzaPrices": { - "broto": "35.00", - "grande": "55.00" - }, - "addons": "yes", - "addonGroups": [] - } - ] - } - ] -} -``` - -**Como identificar pizza no app:** -- Produto pizza: `products[].type === "pizza"`. -- Configuração de montagem/preço por tamanho: `category.isPizzaCategory === true` + `category.pizzaConfig` + `product.pizzaPrices`. - -### 4. Validate Delivery Address (Checkout) -**POST** `/api/store/:storeId/delivery/validate-address` - -Use este endpoint ao trocar endereço na tela de pagamento para validar cobertura antes de finalizar. - -**Body:** - -```json -{ - "address": { - "street": "Avenida das Andorinhas", - "number": "477", - "neighborhood": "Jardim Andorinhas", - "city": "Campinas", - "state": "SP", - "zip": "13101-400", - "lat": -22.918, - "lng": -47.01813 - } -} -``` - -**Resposta Exemplo:** - -```json -{ - "error": false, - "code": "DELIVERY_ADDRESS_VALIDATED", - "result": { - "deliveryAllowed": true, - "reasonCode": "DELIVERY_ALLOWED", - "reasonMessage": "Endereço válido para entrega", - "deliveryMode": "KM", - "distance": 1.3, - "deliveryFee": 8, - "deliveryTime": "35 min", - "sameCity": false, - "matchedKmRange": { "from": 1, "to": 2, "price": 8, "time": 35 }, - "matchedNeighborhood": null - } -} -``` - -**Regras de validação:** -- `KM`: valida distância até o limite da última faixa + tolerância de `0.5 km`. -- `NEIGHBORHOOD`: reprova se cidade do endereço for diferente da cidade da loja. -- `NEIGHBORHOOD`: aprova se bairro configurado bater, ou se for mesma cidade (fallback). - -### 5. Create Order (Checkout) -**POST** `/api/store/:storeId/orders` - -**Body:** - -```json -{ - "customer": { - "name": "Daniel Loverde", - "phone": "11999999999", - "email": "daniel@example.com", - "asaasId": "cus_000005165985" // Opcional. - }, - // Nota: O usuário DEVE estar autenticado via Bearer Token. - // Os dados de 'customer' enviados aqui são usados preferencialmente para - // registrar os dados de entrega/cobrança deste pedido específico no Asaas, - // mas o 'userId' e o histórico são vinculados ao token do usuário logado. - "items": [ - { - "productId": "prod_123", - "name": "X-Burger", - "qty": 2, - "price": 25.00, - "addons": [ - { - "addonId": "addon_egg", - "name": "Ovo", - "price": 2.50, - "qty": 2 - } - ] - } - ], - "total": 50.00, - "paymentMethod": "PIX", // "PIX" | "CREDIT_CARD" | "DEBIT_CARD" | "MONEY" - "deliveryType": "DELIVERY", // "DELIVERY" | "PICKUP" - "address": { - "street": "Rua Exemplo", - "number": "123", - "neighborhood": "Centro" - } -} -``` - -**Exemplo com pizza (tamanho + sabores + borda):** - -```json -{ - "items": [ - { - "productId": "prod_pizza_calabresa", - "name": "Pizza Calabresa", - "qty": 1, - "price": 65.00, - "choices": [ - "Tamanho: Grande (+R$ 55,00)", - "Sabor: Calabresa", - "Massa: Tradicional", - "Borda: Catupiry (+R$ 10,00)" - ] - } - ] -} -``` - -**Como montar `choices[]` para pizza:** - -| Linha | Formato | Obrigatório | -|---|---|---| -| Tamanho | `"Tamanho: (+R$ X,XX)"` | Sim | -| Sabor (único) | `"Sabor: "` | Sim | -| Sabor N (múltiplos) | `"Sabor 1: "`, `"Sabor 2: "` | Sim quando maxFlavors > 1 | -| Massa | `"Massa: "` | Não | -| Borda sem custo | `"Borda: "` | Não | -| Borda com custo | `"Borda: (+R$ X,XX)"` | Não | - -**Regras:** -- `choices[]` é um array de strings. O backend converte para `options[]` na normalização do pedido. -- O `price` do item deve refletir o preço total da pizza (tamanho + modificador de borda). -- Os nomes de tamanho, massa e borda devem corresponder aos `id` ou `name` retornados pelo catalog endpoint. -- Máximo de sabores definido por `pizzaConfig.sizes[n].maxFlavors`. - -**Regras para `items[].addons`:** - -- `qty` do adicional é aceito no backend (ex.: `2 ovos`). -- Se `qty` não for enviado ou vier inválido, a API assume `1`. -- `qty` mínimo efetivo é `1`. - -**Response:** - -```json -{ - "error": false, - "code": "ORDER_CREATED", - "result": { - "id": "ord_987...", - "shortId": "1234", - "status": "PAYMENT_PENDING", - "paymentStatus": "PENDING", - "paymentMethod": "PIX", - "paymentPayload": { - "copyPaste": "000201010212...", - "qrCodeImage": "iVBORw0KGgoAAAANSUhEUgAA...", - "expirationDate": "2026-02-14T22:30:00.000Z" - }, - "payment": { - "method": "PIX", - "status": "PENDING", - "pix": { - "copyPaste": "000201010212...", - "qrCodeImage": "iVBORw0KGgoAAAANSUhEUgAA...", - "expirationDate": "2026-02-14T22:30:00.000Z" - } - } - } -} -``` - -**Observação de segurança:** o Atomenta não retorna mais links de checkout/fatura do Asaas no payload de criação do pedido. - -### 6. Realtime de Status do Pedido (Socket.IO) -Use este canal para acompanhar mudança de status do pedido em tempo real (sem depender de push notification). - -**Socket Endpoint:** -- `ws://:/socket.io/` (em produção, usar `wss://` se houver TLS no balanceador) - -**Auth no handshake:** -- Enviar o JWT do customer em `auth.token` (mesmo token do `Authorization: Bearer ...` das APIs HTTP). - -**Exemplo de conexão (conceitual):** - -```json -{ - "auth": { - "token": "Bearer " - } -} -``` - -**Eventos recebidos pelo app:** -- `order_update`: alteração geral de status do pedido. -- `order_cancelled`: cancelamento confirmado, com motivo e metadados de estorno. -- `delivery_order_cancelled`: evento dedicado para telas de OTP/entrega interromperem o fluxo imediatamente. - -**Payload típico de `order_cancelled`:** - -```json -{ - "event": "order_cancelled", - "emittedAt": "2026-03-05T18:00:00.000Z", - "orderId": "ffdc47b4-6a55-4713-aea9-45169e4cf0d7", - "shortId": "3778", - "storeId": "store_1772117366848_wmqw4", - "userId": "cust_uuid_001", - "status": "CANCELED", - "cancelledBy": "store", - "cancelledAt": "2026-03-05T18:00:00.000Z", - "cancelReasonCode": "STORE_UNAVAILABLE", - "cancelReasonDetail": "Falta de insumo crítico para finalizar o pedido", - "refundStatus": "failed", - "refundIdempotencyKey": "refund:ffdc47b4-...", - "refundProviderRef": null, - "refundError": "Falha ao solicitar estorno automático" -} -``` - -### 7. Fallback oficial (Polling) -Se o socket cair, o app deve continuar consultando o status até estado final. - -**Endpoint:** -- `GET /api/public/orders/:orderId` - -**Sugestão de estratégia:** -- primeiros 60s: a cada `3s` -- próximos 2min: a cada `5s` -- depois: a cada `10s` -- parar em status final (`COMPLETED`, `CANCELED`, `REFUNDED`) ou ao sair da tela. - -### 8. Cancelamento de Pedido (Loja/Customer) - -**Cancelamento pela loja (Store API):** -- `POST /api/store/:storeId/orders/:orderId/cancel` - -**Cancelamento pelo customer:** -- `POST /api/customer/orders/:orderId/cancel` - -**Request body (ambos):** -```json -{ - "reasonCode": "CUSTOMER_REQUEST", - "reasonDetail": "Cliente solicitou cancelamento antes da entrega" -} -``` - -**Motivos aceitos (`reasonCode`):** -- `STORE_UNAVAILABLE` -- `CUSTOMER_REQUEST` -- `DELIVERY_ISSUE` - -**Regras atuais de cancelamento:** -- Permitido em: `PAYMENT_PENDING`, `PENDING`, `ACCEPTED`, `PREPARING`, `READY`, `DELIVERING`. -- Bloqueado em: `COMPLETED` e `CANCELED`. -- Motoboy não cancela pedido. -- Ao cancelar, o backend invalida `otp`, `confirmOtp` e `customerOtp`. - -**Concorrência e idempotência (ponta a ponta):** -- Cancelamento é serializado por `orderId` (lock por pedido). -- Estorno automático usa lock + chave idempotente de estorno para evitar duplicidade. -- Em segunda tentativa após cancelamento efetivado, a API retorna `409 ORDER_ALREADY_CANCELED`. - -**Campos relevantes no retorno:** -- `cancelledBy`, `cancelledAt`, `cancelReasonCode`, `cancelReasonDetail` -- `refundStatus`, `refundIdempotencyKey`, `refundProviderRef` - -### 9. Catálogo Oficial de Tags de Review - -### 9.1 Matriz Completa de Endpoints (Reviews + Tags) - -**Público/App (Customer):** -- `GET /api/public/reviews/tags` → catálogo oficial de tags e regras de validação. -- `POST /api/public/orders/:orderId/review` → cria/atualiza avaliação do pedido concluído. -- `GET /api/public/store/:storeId/reviews` → lista reviews públicos da loja. - -**Store/Backoffice (analytics e operação):** -- `GET /api/store/:storeId/reviews` → listagem detalhada + métricas de reviews da loja. -- `POST /api/store/:storeId/reviews/:reviewId/reply` → resposta oficial da loja para avaliação. -- `POST /api/store/:storeId/reviews/:reviewId/dispute` → contestação da avaliação. -- `GET /api/store/reviews/app/nps` → NPS do app (geral/por filtros). -- `GET /api/store/reviews/app/nps/platform` → NPS por plataforma (`ios`, `android`, `web`). -- `GET /api/store/reviews/observability/overview` → funil técnico (sucesso/rejeição/erro). -- `GET /api/store/reviews/observability/alerts` → alertas operacionais (ex.: pico de reviews negativas). - -**Observação de base URL:** -- No app, consumir sempre via domínio/API oficial do ecossistema (PediFoods/Atomenta), mantendo o Atomenta como orquestrador. - -O app deve buscar este endpoint para renderizar as tags válidas e enviar somente os `id` retornados. - -**Endpoint:** -- `GET /api/public/reviews/tags` - -**Headers:** -- `Accept: application/json` - -**Response (resumo):** - -```json -{ - "error": false, - "result": { - "version": "2026-03-01", - "order": { - "positive": [{ "id": "flavor", "label": "Sabor" }], - "improvement": [{ "id": "wrong_items", "label": "Itens errados" }], - "rules": { - "positiveAllowedWhenRateGte": 5, - "improvementAllowedWhenRateLte": 4 - } - }, - "delivery": { - "sentiments": [ - { "id": "positive", "allowedTags": ["politeness", "on_time"] }, - { "id": "negative", "allowedTags": ["delay", "rude"] } - ], - "positive": [{ "id": "politeness", "label": "Educação" }], - "negative": [{ "id": "delay", "label": "Atraso" }] - }, - "app": { - "nps": { "min": 0, "max": 10 }, - "platforms": ["ios", "android", "web"] - } - } -} -``` - -**Regra de integração:** -- Não hardcodear tags no app; usar o catálogo do backend. -- Enviar no POST de review apenas os `id` retornados em cada bloco. - -### 10. Enviar Avaliação do Pedido -Permite o customer enviar review da loja a partir de um pedido finalizado. - -**Endpoint:** -- `POST /api/public/orders/:orderId/review` - -**Headers:** -- `Authorization: Bearer ` (obrigatório) -- `Accept: application/json` -- `Content-Type: application/json` - -**Body:** - -```json -{ - "orderRate": 5, - "orderComment": "Pedido chegou certinho e bem embalado.", - "orderPositiveTags": ["flavor", "temperature"], - "orderImprovementTags": [], - "deliverySentiment": "positive", - "deliveryPositiveTags": ["on_time", "politeness"], - "deliveryNegativeTags": [], - "appNps": 10, - "platform": "ios", - - "rate": 5, - "message": "Pedido chegou certinho e bem embalado." -} -``` - -**Regras de negócio atuais:** -- Só aceita review para pedido com status `COMPLETED`. -- O `orderId` deve existir. -- `storeId`, `userId`, `clientName` e `orderId` do review são derivados do pedido no backend (não enviar no body). -- Campos obrigatórios para integração nova: `orderRate`, `deliverySentiment`, `appNps`, `platform`. -- Compatibilidade: `rate/message` ainda são aceitos como fallback. -- App deve manter o rascunho local do formulário em caso de erro HTTP para permitir reenvio sem redigitar. - -**Response (sucesso):** - -```json -{ - "error": false, - "result": { - "id": "rev_abc123", - "storeId": "store_1772117366848_wmqw4", - "userId": "cust_uuid_001", - "clientName": "Customer 002", - "rate": 5, - "message": "Pedido chegou certinho e bem embalado.", - "orderRate": 5, - "orderComment": "Pedido chegou certinho e bem embalado.", - "deliveryFeedback": "positive", - "platform": "ios", - "editableUntil": "2026-03-06T17:15:00.000Z", - "storeReplyUntil": "2026-03-06T17:15:00.000Z", - "reviewWindowExpiresAt": "2026-03-06T16:10:00.000Z", - "orderId": "ffdc47b4-6a55-4713-aea9-45169e4cf0d7", - "date": "2026-03-01T17:15:00.000Z" - } -} -``` - -**Erros esperados:** -- `400` quando `orderId` não for enviado. -- `400` quando o pedido não existir ou ainda não estiver concluído (`Apenas pedidos concluídos podem ser avaliados`). -- `400` `REVIEW_WINDOW_EXPIRED` quando passou da janela inicial de 5 dias para criar review. -- `400` `REVIEW_EDIT_WINDOW_EXPIRED` quando tentar alterar review após 5 dias da criação. -- `400` `INVALID_REVIEW_PAYLOAD` / `INVALID_REVIEW_TAGS` / `INVALID_DELIVERY_SENTIMENT_TAGS` para payload inválido. -- `401` se o token JWT for inválido/ausente. -- `500` em falha interna (`Erro ao enviar avaliação`). - -**Exemplos de validação (cenários):** - -Exemplo válido (nota alta + entrega positiva): - -```json -{ - "orderRate": 5, - "orderComment": "Perfeito.", - "orderPositiveTags": ["flavor", "temperature"], - "orderImprovementTags": [], - "deliverySentiment": "positive", - "deliveryPositiveTags": ["on_time", "politeness"], - "deliveryNegativeTags": [], - "appNps": 10, - "platform": "android" -} -``` - -Exemplo inválido (nota 5 com `orderImprovementTags`): - -```json -{ - "orderRate": 5, - "orderImprovementTags": ["temperature"], - "deliverySentiment": "positive", - "appNps": 8, - "platform": "ios" -} -``` - -Retorno esperado: `400 INVALID_REVIEW_TAGS`. - -Exemplo inválido (entrega positiva com tags negativas): - -```json -{ - "orderRate": 4, - "orderImprovementTags": ["temperature"], - "deliverySentiment": "positive", - "deliveryNegativeTags": ["delay"], - "appNps": 7, - "platform": "web" -} -``` - -Retorno esperado: `400 INVALID_DELIVERY_SENTIMENT_TAGS`. - -### 11. Analytics de Reviews (Store/API) -Para dashboard da loja e análise de produto, usar: - -- `GET /api/store/:storeId/reviews` - - filtros opcionais: `startDate`, `endDate`, `minRate`, `maxRate`, `deliverySentiment`, `platform` - - retorno inclui métricas de estrelas, blocos de pedido/entrega, janelas (`editableUntil`, `storeReplyUntil`) e `appNps` agregado. - -- `POST /api/store/:storeId/reviews/:reviewId/reply` - - body obrigatório: `reply` (texto da resposta da loja). - - regra: respeita janela de resposta (`storeReplyUntil`). - -- `POST /api/store/:storeId/reviews/:reviewId/dispute` - - body obrigatório: `reason` (motivo da contestação). - -- `GET /api/store/reviews/app/nps` - - filtros opcionais: `startDate`, `endDate`, `storeId`, `platform` - - retorno: volume NPS, média e score. - -- `GET /api/store/reviews/app/nps/platform` - - filtros opcionais: `startDate`, `endDate`, `storeId` - - retorno: NPS separado em `ios`, `android`, `web`. - -- `GET /api/store/reviews/observability/overview` - - filtros opcionais: `startDate`, `endDate`, `storeId` - - retorno: tentativas, sucesso/rejeição/erro, taxa de sucesso e conversão por pedidos concluídos. - -- `GET /api/store/reviews/observability/alerts` - - filtro opcional: `storeId` - - retorno: alertas de pico, ex.: `NEGATIVE_REVIEWS_SPIKE`. - -**Compatibilidade Store:** -- O painel `/store/reviews` segue funcional com os campos legados (`rate`, `message`, `itemFeedback`, `improvementFeedback`, `deliveryFeedback`). -- Campos novos coexistem para evolução gradual sem quebrar o fluxo atual. - ---- - -## Feature Control (Feature Flags) - -O Feature Control permite ativar/desativar funcionalidades do app remotamente sem publicar uma nova versão. O app nunca chama o Atomenta diretamente — usa um **BFF (Backend for Frontend)** que protege o `Atomenta-Token` do cliente. - -### Arquitetura - -``` -App iOS → POST /feature-control/bootstrap → BFF → Atomenta /api/feature-control/evaluate -``` - -O BFF mantém cache de 60 segundos. Em caso de timeout ou erro upstream, retorna flags de fallback estáticas definidas nas variáveis de ambiente do BFF. - ---- - -### 1. Bootstrap — Avaliar flags na inicialização - -**`POST /feature-control/bootstrap`** - -Deve ser chamado uma vez por sessão do app (ao abrir ou ao autenticar). Retorna os valores de todos os flags solicitados para o contexto do usuário. - -**Headers:** -``` -Content-Type: application/json -Authorization: Bearer ← opcional, mas recomendado -``` - -> Se o `Authorization` for enviado, o BFF extrai `subjectId` automaticamente do JWT (`sub` / `customerId` / `id`). Se não for enviado, `context.subjectId` é obrigatório. - -**Request body:** -```json -{ - "environment": "production", - "keys": [ - "at.ios.only", - "at.android.only", - "at.promo", - "at.city.aguai" - ], - "context": { - "subjectType": "customer", - "subjectId": "e30188cf-cf69-471e-bc59-4c92e0f9ad7b", - "platform": "ios", - "appVersion": "2.3.1", - "attributes": { - "city": "Aguaí" - } - } -} -``` - -| Campo | Tipo | Obrigatório | Descrição | -|---|---|---|---| -| `environment` | string | não | `"production"` (default) ou `"sandbox"` | -| `keys` | string[] | sim | Lista de flags a avaliar (máx 100) | -| `context.subjectType` | string | sim | `"customer"` \| `"user"` \| `"store"` \| `"anonymous"` | -| `context.subjectId` | string | sim* | ID do usuário. *Dispensável se JWT enviado no header | -| `context.platform` | string | não | `"ios"` ou `"android"` | -| `context.appVersion` | string | não | Versão semântica, ex: `"2.3.1"` | -| `context.storeId` | string | não | Para flags segmentadas por loja | -| `context.attributes` | object | não | Atributos extras (ex: `city`, `tier`) | - -**Response 200 — fonte live:** -```json -{ - "ok": true, - "source": "live", - "configVersion": 7, - "evaluatedAt": "2026-06-05T12:00:00.000Z", - "flags": { - "atiosonly": true, - "atandroidonly": false, - "atpromo": true, - "atcityaguai": true - }, - "raw": { - "at.ios.only": { - "enabled": true, - "variant": "on", - "payload": null, - "reason": "rollout" - }, - "at.promo": { - "enabled": true, - "variant": "on", - "payload": null, - "reason": "rollout" - }, - "at.city.aguai": { - "enabled": true, - "variant": "on", - "payload": null, - "reason": "segment" - }, - "at.android.only": { - "enabled": false, - "variant": "off", - "payload": null, - "reason": "default" - } - } -} -``` - -**Response 200 — fonte fallback** (quando BFF não consegue atingir Atomenta): -```json -{ - "ok": true, - "source": "fallback", - "configVersion": 0, - "evaluatedAt": "2026-06-05T12:00:00.000Z", - "flags": { "ios": true, "promo": true }, - "raw": { ... }, - "upstreamError": { - "status": 504, - "code": "FEATURE_CONTROL_UPSTREAM_TIMEOUT", - "message": "Timeout no upstream" - } -} -``` - -#### Como usar `raw` no app (Swift) - -O app usa o campo **`raw`**, indexado pela chave original completa. O campo `flags` contém uma versão simplificada (pontos removidos), mas o app Swift usa `raw` para manter compatibilidade direta com os nomes de flag. - -| Chave original | Acesso via `raw` | `flags` simplificado | -|---|---|---| -| `at.ios.only` | `raw["at.ios.only"]` | `flags["atiosonly"]` | -| `at.android.only` | `raw["at.android.only"]` | `flags["atandroidonly"]` | -| `at.promo` | `raw["at.promo"]` | `flags["atpromo"]` | -| `at.city.aguai` | `raw["at.city.aguai"]` | `flags["atcityaguai"]` | - -> **Use sempre `raw`** para acessar flags no app — as chaves simplificadas em `flags` são geradas mecanicamente e menos legíveis. - -**Regra de leitura em `raw`:** -- `raw["at.promo"].enabled == true` → flag habilitado -- `raw["at.promo"].variant == "on"` → variante ativa -- `raw["at.promo"].payload` → dados extras opcionais (JSON livre) - -**Exemplo Swift (padrão atual do app):** -```swift -// FeatureFlagsState.isEnabled() já lê raw automaticamente -if appState.featureFlags.isEnabled("at.promo") { - // mostrar módulo de promoções -} - -if appState.featureFlags.isEnabled("at.ios.only") { - // funcionalidade exclusiva iOS -} -``` - -#### `source` e estratégia de fallback - -| `source` | Significado | Ação recomendada | -|---|---|---| -| `"live"` | Dados frescos do Atomenta | Usar normalmente | -| `"fallback"` | BFF usou defaults estáticos (timeout/erro) | Usar com cautela; re-tentar na próxima sessão | - ---- - -### 2. Telemetria de Exposição - -**`POST /feature-control/telemetry/exposure`** - -Registra quais flags o usuário visualizou (para análise de rollout). Envio em batch, **fire-and-forget** — não bloquear UX aguardando resposta. - -**Headers:** -``` -Content-Type: application/json -Authorization: Bearer -``` - -**Request body:** -```json -{ - "events": [ - { - "featureKey": "at.promo", - "variant": "on", - "subjectType": "customer", - "storeId": "store_1777215327582_akum6" - }, - { - "featureKey": "at.ios.only", - "variant": "on", - "subjectType": "customer" - } - ] -} -``` - -| Campo | Tipo | Obrigatório | Descrição | -|---|---|---|---| -| `featureKey` | string | sim | Chave original com prefixo `fc.` | -| `variant` | string | não | Variante exposta (default `"off"`) | -| `subjectType` | string | não | Default `"customer"` | -| `storeId` | string | não | Quando relevante | - -Máximo de **100 eventos por request**. - -**Response 200:** -```json -{ - "ok": true, - "code": "FEATURE_CONTROL_EXPOSURE_ACCEPTED", - "result": { "count": 2 } -} -``` - ---- - -### 3. Flags disponíveis (referência) - -Padrão de nomenclatura: `at..` (pontos como separador, sem hífens). - -| Flag | Descrição | Default | -|---|---|---| -| `at.ios.only` | Funcionalidades exclusivas iOS | `true` | -| `at.android.only` | Funcionalidades exclusivas Android | `true` | -| `at.promo` | Módulo de promoções ativo | `true` | -| `at.city.aguai` | Expansão para cidade de Aguaí/SP | `true` | - -> Novos flags são criados pelo time Pedi Foods no painel `/feature-control`. Consulte o time antes de codificar um flag inexistente. A lista completa de flags ativos está disponível no painel admin → Feature Control. - ---- - -### 4. Fluxo recomendado no app - -``` -1. App abre / usuário autentica -2. POST /feature-control/bootstrap (com JWT no header) -3. Salvar flags em memória/UserDefaults para a sessão -4. Renderizar UI baseada nos flags -5. POST /feature-control/telemetry/exposure (fire-and-forget, por flag visualizado) -6. Ao fechar sessão ou após 15 min, repetir passo 2 na próxima abertura -``` - -> **Não** chamar bootstrap a cada tela — apenas na inicialização da sessão. O BFF tem cache de 60s no servidor; o app deve ter cache local mínimo de 15 minutos para evitar latência desnecessária. - ---- - -Autor: Daniel Arantes Loverde diff --git a/API_Store_External.md b/API_Store_External.md deleted file mode 100644 index f0a4174..0000000 --- a/API_Store_External.md +++ /dev/null @@ -1,642 +0,0 @@ -# Atomenta API - Store Management & External Guide (Pedi Foods Edition) - -Este documento é o guia definitivo e completo para parceiros e aplicações externas (como pedifoods.com.br) que consomem a API da plataforma Atomenta para gestão de lojas. **Esta API reflete 100% das funcionalidades do painel administrativo.** - -## 🔐 Autenticação e Headers - -Todas as chamadas devem usar autenticação híbrida. - -### Headers Obrigatórios: -- **Accept**: `application/json` -- **Atomenta-Token**: `550e8400-e29b-41d4-a716-446655440008` (Escopo: Store Management) -- **Authorization**: `Bearer ` - ---- - -## 🏬 Gestão de Identidade e Lojas - -### 1. Listar Lojas do Usuário -**GET** `/api/store/list` - Retorna todas as lojas vinculadas ao token do usuário. - -### 2. Informações Completas da Loja (Settings) -**GET** `/api/store/:storeId/info` - Retorna a configuração completa da loja, incluindo dados bancários, horários e flags de pagamento. - -**Resposta Exemplo:** -```json -{ - "error": false, - "result": { - "isOpen": true, - "statusLabel": "Aberto Agora", - "isManualOpen": true, - "fantasyName": "Pizzaria do Arantes", - "razaoSocial": "Loverde Co LTDA", - "document": "12.345.678/0001-99", - "documentType": "CNPJ", - "email": "loja@loverde.com.br", - "phone": "11988776655", - "paymentMethods": { - "paymentOnDelivery": true, - "acceptCash": true, - "acceptCreditVisa": true, - "acceptCreditMaster": true, - "acceptCreditElo": true, - "acceptVoucherAlelo": true, - "acceptPix": true - }, - "bankInfo": { - "bankName": "Banco do Brasil", - "accountType": "Corrente", - "agency": "1234", - "account": "56789-0", - "pixKey": "12345678000199" - }, - "openingHours": { - "monday": [{"open": "18:00", "close": "23:00"}], - "friday": [{"open": "11:00", "close": "15:00"}, {"open": "18:00", "close": "00:00"}] - }, - "address": { - "street": "Av. Brasil, 1000", - "latitude": -23.55052, - "longitude": -46.633308 - } - } -} -``` - ---- - -## 📊 Dashboard Real-time (Portal Parity) - -### 3. Dashboard e Monitoramento -**GET** `/api/store/:storeId/dashboard` - -**Resposta Detalhada (100% Parity):** -```json -{ - "error": false, - "result": { - "orderFlow": { - "urgencys": [ { "id": "ORD-123", "clientName": "Daniel", "minutes": 45, "status": "PREPARING" } ], - "received": [ /* Pedidos Pendentes */ ], - "inProgress": [ /* Pedidos Em Preparo */ ], - "readyTo": [ /* Pedidos Prontos para Entrega/Retirada */ ], - "inRoute": [ /* Pedidos em Rota */ ], - "delivered": [ /* Pedidos Finalizados na Sessão */ ] - }, - "payments": { - "totalSelledToday": 2450.00, - "growthToday": 15.5, - "avgTicket": 65.20, - "growthPeriod": 8.5 - }, - "alerts": { - "pendingOver3Minutes": 2, - "delayedInProgress": 1 - } - } -} -``` - ---- - -## 🍴 Catálogo e Menu (Complex Payloads) - -### 3.1 Catálogo Consolidado (App) -**GET** `/api/store/:storeId/catalog` - -**Resposta:** -```json -{ - "error": false, - "code": "STORE_CATALOG_RETRIEVED", - "result": [ - { - "id": "cat_lanches", - "name": "Lanches", - "isPizzaCategory": false, - "products": [ - { - "id": "prod_xburger", - "type": "prepared", - "name": "X-Burger", - "description": "Pão, carne e queijo", - "image": "/uploads/products/xburger.jpg", - "price": "25.00", - "addons": "yes", - "addonGroups": [] - } - ] - }, - { - "id": "cat_pizzas", - "name": "Pizzas", - "isPizzaCategory": true, - "pizzaConfig": { - "sizes": [{ "id": "broto", "name": "Broto", "slices": 4, "maxFlavors": 1 }], - "doughs": [{ "id": "tradicional", "name": "Tradicional", "active": true }], - "crusts": [{ "id": "catupiry", "name": "Catupiry", "active": true, "priceModifier": "10.00" }] - }, - "products": [ - { - "id": "prod_pizza_calabresa", - "type": "pizza", - "name": "Pizza Calabresa", - "description": "Molho, mussarela e calabresa", - "image": "/uploads/products/pizza-calabresa.jpg", - "price": "0.00", - "pizzaPrices": { - "broto": "35.00", - "grande": "55.00" - }, - "addons": "yes", - "addonGroups": [] - } - ] - } - ] -} -``` - -**Regra de identificação de pizza:** -- Item pizza: `products[].type === "pizza"`. -- Configuração de montagem vem em `category.pizzaConfig`. -- Preço por tamanho vem em `product.pizzaPrices`. - -### 4. Listar Categorias do Catálogo -**GET** `/api/store/:storeId/catalog/categories` - -**Resposta:** -```json -{ - "error": false, - "code": "CATEGORIES_RETRIEVED", - "result": [ - { "id": "cat_1", "name": "Pizzas", "isActive": true } - ], - "message": "Categorias recuperadas com sucesso" -} -``` - -### 5. Criar Categoria (Ex.: Pizza) -**POST** `/api/store/:storeId/catalog/categories` -```json -{ - "name": "Pizzas Salgadas", - "isPizzaCategory": true, - "pizzaConfig": { - "sizes": [ - { "id": "small", "name": "Broto", "slices": 4, "maxFlavors": 1 }, - { "id": "large", "name": "Grande", "slices": 8, "maxFlavors": 3 } - ], - "doughs": [ { "id": "tradicional", "name": "Tradicional", "price": 0 } ], - "crusts": [ { "id": "catupiry", "name": "Borda de Catupiry", "price": 10.00 } ] - } -} -``` - -### 6. Atualizar Categoria -**PUT** `/api/store/:storeId/catalog/categories/:id` - -### 7. Excluir Categoria -**DELETE** `/api/store/:storeId/catalog/categories/:id` - -### 8. Produtos Tipo Combo ou Pizza -**POST** `/api/store/:storeId/catalog/products` -```json -{ - "type": "pizza", - "name": "Pizza de Calabresa", - "categoryId": "cat_pizzas", - "pizzaPrices": { - "small": "35.00", - "large": "55.00" - }, - "addonGroups": [ - { - "name": "Remover Ingredientes", - "minSelectors": 0, "maxSelectors": 5, - "items": [ { "name": "Sem Cebola", "price": 0 } ] - } - ] -} -``` - ---- - -## 📦 Histórico e Detalhes do Pedido - -### 9. Histórico com Filtros -**GET** `/api/store/:storeId/orders/history?start=2024-01-01&end=2024-01-31&q=Ana` - -**Objeto de Pedido Completo (result.list[0]):** -```json -{ - "id": "order_guid", - "shortId": "1234", - "clientName": "Arantes Loverde", - "totalValue": 120.50, - "netValue": 112.40, - "pediFoodsFee": 1.00, - "status": "COMPLETED", - "createdAt": "2024-01-08T15:00:00Z", - "confirmOtp": "12345678", - "deliveryEstimate": "35-50 min", - "deliveryTypeLabel": "Entrega Própria", - "timeline": [ - { "status": "PENDING", "time": "15:00:00", "message": "Pedido criado" }, - { "status": "ACCEPTED", "time": "15:02:00", "message": "Chef aceitou o pedido" }, - { "status": "COMPLETED", "time": "15:45:00", "message": "Entregue ao cliente" } - ], - "items": [ - { "name": "Pizza Grande", "price": 55.00, "choices": ["Borda Catupiry", "Meio Calabresa", "Meio Mussarela"] } - ] -} -``` - -> [!IMPORTANT] -> O campo `confirmOtp` (8 dígitos) é o código que o motoboy deve utilizar para iniciar a confirmação. O `customerOtp` (4 dígitos do cliente) **NÃO** é retornado via API por questões de segurança. -``` - -### 10. Detalhes de um Pedido Individual -**GET** `/api/store/:storeId/orders/:orderId` - -Retorna o objeto completo do pedido (parity 100% com o painel). - ---- - -## 💰 Financeiro Detalhado - -### 11. Resumo de Performance e Gateway -**GET** `/api/store/:storeId/financial/summary` - -**Resposta:** -```json -{ - "error": false, - "result": { - "performance": { - "totalSelledAllTime": 150230.50, - "avgTicket": 72.00 - }, - "gateway": { - "balance": { "balance": 1500.00, "available": 1200.00 }, - "transfers": [ - { "id": "TRANS-1", "date": "2024-01-05", "value": 500.00, "status": "DONE" } - ] - } - } -} -``` - ---- - -## ⭐ Reviews e Avaliações - -### 12. Métricas e Respostas -**GET** `/api/store/:storeId/reviews` - -**Resposta:** -```json -{ - "error": false, - "result": { - "metrics": { - "averageRate": "4.1", - "totalReviews": 260, - "starsDistribution": { "5": 121, "4": 60, "3": 23, "2": 15, "1": 39 }, - "itemFeedback": { - "flavor": 207, - "ingredients": 94, - "packaging": 35, - "temperature": 29, - "appearance": 23 - }, - "improvementFeedback": { - "quantity": 128, - "temperature": 89, - "packaging": 42, - "ingredients": 23, - "flavor": 13 - }, - "deliveryFeedback": { - "positive": 102, - "negative": 60 - } - }, - "list": [ - { - "id": "rev_1", - "clientName": "Ana", - "rate": 5, - "message": "Melhor pizza!", - "reply": "Obrigado Ana!", - "repliedAt": "2024-01-08T16:00:00Z" - } - ] - } -} -``` - -### 9. Contestar Avaliação -**POST** `/api/store/:storeId/reviews/:reviewId/dispute` -```json -{ "reason": "Cliente agressivo e mensagem falsa sobre o produto." } -``` - ---- - -## 🖼️ Upload de Ativos e Galeria -**POST** `/api/store/:storeId/assets/upload` (Form-data: `file`) -> Retorna URL absoluta. - -**GET** `/api/store/:storeId/assets` -> Retorna lista de URLs absolutas de todas as imagens da loja (logo, capa e produtos). - ---- - -## 🏷️ Reordenação de Catálogo -**POST** `/api/store/:storeId/catalog/products/reorder` (Payload: `{ "ids": ["p1", "p2", ...] }`) -**POST** `/api/store/:storeId/catalog/categories/reorder` (Payload: `{ "ids": ["c1", "c2", ...] }`) - ---- - -## 🛵 Fluxo de Entrega Pedi Foods (2-Step OTP) - -| Campo | Tipo | Descrição | -|---|---|---| -| `otp` | String | Legacy OTP (4 digits). | -| `confirmOtp` | String | **Novo:** Código de 8 dígitos para o motoboy. (Use este para o fluxo de 2 etapas) | -| `deliveryEstimate` | String | Tempo estimado de entrega (ex: "30-45 min"). | -| `pediFoodsFee` | Number | Taxa de serviço Pedi Foods calculada. | - -## 8. Entrega e Confirmação (Motoboy Flow) - -Novos endpoints públicos para confirmação de entrega em duas etapas. - -### 8.1. Página de Confirmação -`GET /delivery/confirm/:orderId` -Renderiza a página para o motoboy iniciar o processo. - -### 8.2. Validar Motoboy (Passo 1) -`POST /delivery/confirm/:orderId/validate-motoboy` -Valida o código de 8 dígitos (`confirmOtp`). - -**Payload:** -```json -{ "otp": "12345678" } -``` - -### 8.3. Validar Cliente (Passo 2) -`POST /delivery/confirm/:orderId/validate-customer` -Valida o código de 4 dígitos (`customerOtp`) e finaliza o pedido (`COMPLETED`). - -**Payload:** -```json -{ "otp": "1234" } -``` -Para garantir a segurança da entrega, o sistema utiliza um fluxo de confirmação em duas etapas via página pública: - -1. **URL de Confirmação**: `https://atomenta.loverde.com.br/delivery/confirm/:orderRealId` -2. **Passo 1 (Motoboy)**: O entregador insere o `confirmOtp` (8 dígitos) visível no painel da loja. -3. **Passo 2 (Cliente)**: Após validar o código do motoboy, o sistema solicita o `customerOtp` (4 dígitos) que o cliente possui em seu app/notificação. -4. **Finalização**: A validação bem-sucedida do segundo código altera o status do pedido para `COMPLETED` automaticamente. - -Endpoints utilizados pela página pública (CORS habilitado para domínios parceiros): -- `POST /delivery/confirm/:orderId/validate-motoboy` { "otp": "String(8)" } -- `POST /delivery/confirm/:orderId/validate-customer` { "otp": "String(4)" } - ---- - -## 🛒 Checkout e Criação de Pedido -### 10.0 Validar Endereço de Entrega (Checkout) -**POST** `/api/store/:storeId/delivery/validate-address` - -Endpoint para validar cobertura de entrega no checkout, quando o cliente troca o endereço antes do pagamento. - -**Payload:** -```json -{ - "address": { - "street": "Avenida das Andorinhas", - "number": "477", - "neighborhood": "Jardim Andorinhas", - "city": "Campinas", - "state": "SP", - "zip": "13101-400", - "lat": -22.918, - "lng": -47.01813 - } -} -``` - -**Resposta Exemplo:** -```json -{ - "error": false, - "code": "DELIVERY_ADDRESS_VALIDATED", - "result": { - "deliveryAllowed": true, - "reasonCode": "DELIVERY_ALLOWED", - "reasonMessage": "Endereço válido para entrega", - "deliveryMode": "KM", - "distance": 1.3, - "deliveryFee": 8, - "deliveryTime": "35 min", - "sameCity": false, - "matchedKmRange": { "from": 1, "to": 2, "price": 8, "time": 35 }, - "matchedNeighborhood": null - } -} -``` - -**Regras:** -- `KM`: aprova somente até o limite da última faixa + `0.5 km` de tolerância. -- `NEIGHBORHOOD`: reprova se a cidade do endereço for diferente da cidade da loja. -- `NEIGHBORHOOD`: aprova com bairro configurado ou com fallback de mesma cidade. - -### 10. Criar Novo Pedido (Public) -**POST** `/api/public/orders` - -Este endpoint é utilizado pelo front-end de checkout (Pedi Foods) para criar o pedido no sistema da loja. - -**Payload:** -```json -{ - "storeId": "store_123456", - "userId": "user_guid_or_guest", - "clientName": "João Silva", - "clientPhone": "11999999999", - "deliveryType": "DELIVERY", // ou "PICKUP" - "address": { - "street": "Rua das Flores", - "number": "123", - "neighborhood": "Centro", - "city": "São Paulo", - "state": "SP", - "zip": "01000-000", - "complement": "Apt 10", - "lat": -23.55, - "lng": -46.63 - }, - "paymentMethod": "PIX", // "CREDIT_CARD", "DEBIT_CARD", "CASH" - "itemsTotal": 50.00, - "deliveryFee": 5.00, - "serviceFee": 0.00, - "discount": 0.00, - "total": 55.00, - "items": [ - { - "id": "prod_1", - "name": "Pizza", - "price": 50.00, - "qty": 1, - "image": "...", - "addons": [ - { - "addonId": "addon_bacon", - "name": "Bacon", - "price": 4.00, - "qty": 2 - } - ] - } - ] -} -``` - -**Regras para `items[].addons`:** -- `qty` do adicional é suportado (ex.: 2x bacon/ovo). -- Se `qty` não for enviado ou vier inválido, a API assume `1`. -- `qty` mínimo efetivo é `1`. - -**Resposta Sucesso:** -```json -{ - "error": false, - "result": { - "id": "order_guid", - "shortId": "1234", - "status": "PAYMENT_PENDING", - "paymentStatus": "PENDING", - "paymentMethod": "PIX", - "paymentPayload": { - "copyPaste": "000201010212...", - "qrCodeImage": "iVBORw0KGgoAAAANSUhEUgAA...", - "expirationDate": "2026-02-14T22:30:00.000Z" - }, - "payment": { - "method": "PIX", - "status": "PENDING", - "pix": { - "copyPaste": "000201010212...", - "qrCodeImage": "iVBORw0KGgoAAAANSUhEUgAA...", - "expirationDate": "2026-02-14T22:30:00.000Z" - } - } - } -} -``` - -**Observação de segurança:** o Atomenta não retorna links de checkout/fatura do Asaas no response de criação de pedido. - -### 10.1 Realtime de Status de Pedido (Socket.IO) -Para atualização em tempo real do checkout/app (sem depender de push), usar Socket.IO. - -**Socket endpoint:** -- `ws://:/socket.io/` (usar `wss://` em produção com TLS) - -**Auth no handshake:** -- Enviar JWT no campo `auth.token`. -- Exemplo: `Bearer `. - -**Evento para o cliente:** -- `order_update` - -**Payload típico do evento:** -```json -{ - "id": "order_guid", - "shortId": "1234", - "storeId": "store_123456", - "userId": "user_guid", - "status": "CONFIRMED", - "paymentStatus": "CONFIRMED", - "updatedAt": "2026-02-15T20:10:00.000Z" -} -``` - -### 10.2 Fallback de Status (Polling) -Caso não haja conexão socket, consultar status por HTTP até estado final. - -**Endpoint:** -- `GET /api/public/orders/:orderId` - -**Recomendação de intervalo:** -- 0-60s: `3s` -- 60-180s: `5s` -- acima de 180s: `10s` -- parar em status final (`COMPLETED`, `CANCELED`, `REFUNDED`). - -### 11. Listar Lojas (Public App) -**GET** `/api/public/stores` - -Endpoint utilizado pelo aplicativo Pedi Foods para listar todas as lojas disponíveis para o cliente. - -**Resposta Sucesso:** -```json -{ - "error": false, - "result": [ - { - "id": "company_guid", - "storeId": "store_123456", - "name": "Pizzaria do Arantes", - "logo": "https://...", - "cover": "https://...", - "category": "Restaurante", - "isOpen": true, - "statusLabel": "Aberto Agora", - "rating": 4.8, - "deliveryTime": "30-45 min", - "deliveryFee": 5.00 - } - ] -} -``` - ---- - -## 📝 Gestão de Contratos (Store) - -### 12. Verificar Status do Contrato -**GET** `/api/store/:storeId/contract/status` -Retorna o ID e o status do contrato mais recente da loja. - -**Resposta Exemplo:** -```json -{ - "error": false, - "result": { - "id": "contract_guid", - "status": "draft" // ou "signed" - } -} -``` - -### 13. Assinar Contrato -**POST** `/api/store/:storeId/contract/sign` -Assina o contrato draft atual da loja. - -**Payload:** -```json -{ - "method": "typed", - "typedName": "João Silva", // Obrigatório se method === "typed" - "signatureDataUrl": "data:image/png;base64,..." // Obrigatório se method === "draw" -} -``` - -### 14. Visualizar PDF do Contrato -**GET** `/api/store/contract/:contractId/pdf` -Retorna o stream do arquivo PDF do contrato (draft ou assinado). - -Autor: Daniel Arantes Loverde diff --git a/pedi-foods/Darwin/Assets.xcassets/AccentColor.colorset/Contents.json b/Darwin/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/AccentColor.colorset/Contents.json rename to Darwin/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/Contents.json b/Darwin/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Darwin/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_1024x1024 1.png b/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_1024x1024 1.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_1024x1024 1.png rename to Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_1024x1024 1.png diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_1024x1024.png b/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_1024x1024.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_1024x1024.png rename to Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_1024x1024.png diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_dark_1024x1024.png b/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_dark_1024x1024.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_dark_1024x1024.png rename to Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_dark_1024x1024.png diff --git a/pedi-foods/Darwin/Assets.xcassets/Contents.json b/Darwin/Assets.xcassets/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/Contents.json rename to Darwin/Assets.xcassets/Contents.json diff --git a/pedi-foods/Darwin/Assets.xcassets/LaunchBackground.colorset/Contents.json b/Darwin/Assets.xcassets/LaunchBackground.colorset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/LaunchBackground.colorset/Contents.json rename to Darwin/Assets.xcassets/LaunchBackground.colorset/Contents.json diff --git a/pedi-foods/Darwin/Assets.xcassets/alelocard_logo.imageset/Contents.json b/Darwin/Assets.xcassets/alelocard_logo.imageset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/alelocard_logo.imageset/Contents.json rename to Darwin/Assets.xcassets/alelocard_logo.imageset/Contents.json diff --git a/logo/alelocard_logo.png b/Darwin/Assets.xcassets/alelocard_logo.imageset/alelocard_logo.png similarity index 100% rename from logo/alelocard_logo.png rename to Darwin/Assets.xcassets/alelocard_logo.imageset/alelocard_logo.png diff --git a/pedi-foods/Darwin/Assets.xcassets/amexcard_logo.imageset/Contents.json b/Darwin/Assets.xcassets/amexcard_logo.imageset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/amexcard_logo.imageset/Contents.json rename to Darwin/Assets.xcassets/amexcard_logo.imageset/Contents.json diff --git a/logo/amexcard_logo.png b/Darwin/Assets.xcassets/amexcard_logo.imageset/amexcard_logo.png similarity index 100% rename from logo/amexcard_logo.png rename to Darwin/Assets.xcassets/amexcard_logo.imageset/amexcard_logo.png diff --git a/pedi-foods/Darwin/Assets.xcassets/hipercard_logo.imageset/Contents.json b/Darwin/Assets.xcassets/hipercard_logo.imageset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/hipercard_logo.imageset/Contents.json rename to Darwin/Assets.xcassets/hipercard_logo.imageset/Contents.json diff --git a/logo/hipercard_logo.png b/Darwin/Assets.xcassets/hipercard_logo.imageset/hipercard_logo.png similarity index 100% rename from logo/hipercard_logo.png rename to Darwin/Assets.xcassets/hipercard_logo.imageset/hipercard_logo.png diff --git a/pedi-foods/Darwin/Assets.xcassets/icon_pix.imageset/Contents.json b/Darwin/Assets.xcassets/icon_pix.imageset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/icon_pix.imageset/Contents.json rename to Darwin/Assets.xcassets/icon_pix.imageset/Contents.json diff --git a/logo/icon_pix.png b/Darwin/Assets.xcassets/icon_pix.imageset/icon_pix.png similarity index 100% rename from logo/icon_pix.png rename to Darwin/Assets.xcassets/icon_pix.imageset/icon_pix.png diff --git a/pedi-foods/Darwin/Assets.xcassets/mastercard_logo.imageset/Contents.json b/Darwin/Assets.xcassets/mastercard_logo.imageset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/mastercard_logo.imageset/Contents.json rename to Darwin/Assets.xcassets/mastercard_logo.imageset/Contents.json diff --git a/logo/mastercard_logo.png b/Darwin/Assets.xcassets/mastercard_logo.imageset/mastercard_logo.png similarity index 100% rename from logo/mastercard_logo.png rename to Darwin/Assets.xcassets/mastercard_logo.imageset/mastercard_logo.png diff --git a/pedi-foods/Darwin/Assets.xcassets/pedifoods.imageset/Contents.json b/Darwin/Assets.xcassets/pedifoods.imageset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/pedifoods.imageset/Contents.json rename to Darwin/Assets.xcassets/pedifoods.imageset/Contents.json diff --git a/logo/pedifoods.png b/Darwin/Assets.xcassets/pedifoods.imageset/pedifoods.png similarity index 100% rename from logo/pedifoods.png rename to Darwin/Assets.xcassets/pedifoods.imageset/pedifoods.png diff --git a/logo/pedifoods_dark.png b/Darwin/Assets.xcassets/pedifoods.imageset/pedifoods_dark.png similarity index 100% rename from logo/pedifoods_dark.png rename to Darwin/Assets.xcassets/pedifoods.imageset/pedifoods_dark.png diff --git a/pedi-foods/Darwin/Assets.xcassets/pin_image_app.imageset/Contents.json b/Darwin/Assets.xcassets/pin_image_app.imageset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/pin_image_app.imageset/Contents.json rename to Darwin/Assets.xcassets/pin_image_app.imageset/Contents.json diff --git a/pedi-foods/Android/app/src/main/res/drawable/pin_image_app.png b/Darwin/Assets.xcassets/pin_image_app.imageset/pin_image_app.png similarity index 100% rename from pedi-foods/Android/app/src/main/res/drawable/pin_image_app.png rename to Darwin/Assets.xcassets/pin_image_app.imageset/pin_image_app.png diff --git a/pedi-foods/Android/app/src/main/res/drawable/pin_image_app_dark.png b/Darwin/Assets.xcassets/pin_image_app.imageset/pin_image_app_dark.png similarity index 100% rename from pedi-foods/Android/app/src/main/res/drawable/pin_image_app_dark.png rename to Darwin/Assets.xcassets/pin_image_app.imageset/pin_image_app_dark.png diff --git a/pedi-foods/Darwin/Assets.xcassets/placeholder-pizza.imageset/Contents.json b/Darwin/Assets.xcassets/placeholder-pizza.imageset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/placeholder-pizza.imageset/Contents.json rename to Darwin/Assets.xcassets/placeholder-pizza.imageset/Contents.json diff --git a/pedi-foods/Darwin/Assets.xcassets/placeholder-pizza.imageset/placeholder-pizza.png b/Darwin/Assets.xcassets/placeholder-pizza.imageset/placeholder-pizza.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/placeholder-pizza.imageset/placeholder-pizza.png rename to Darwin/Assets.xcassets/placeholder-pizza.imageset/placeholder-pizza.png diff --git a/pedi-foods/Darwin/Assets.xcassets/placeholder-product.imageset/Contents.json b/Darwin/Assets.xcassets/placeholder-product.imageset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/placeholder-product.imageset/Contents.json rename to Darwin/Assets.xcassets/placeholder-product.imageset/Contents.json diff --git a/pedi-foods/Darwin/Assets.xcassets/placeholder-product.imageset/placeholder-product.png b/Darwin/Assets.xcassets/placeholder-product.imageset/placeholder-product.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/placeholder-product.imageset/placeholder-product.png rename to Darwin/Assets.xcassets/placeholder-product.imageset/placeholder-product.png diff --git a/pedi-foods/Darwin/Assets.xcassets/sodexo_logo.imageset/Contents.json b/Darwin/Assets.xcassets/sodexo_logo.imageset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/sodexo_logo.imageset/Contents.json rename to Darwin/Assets.xcassets/sodexo_logo.imageset/Contents.json diff --git a/logo/sodexo_logo.png b/Darwin/Assets.xcassets/sodexo_logo.imageset/sodexo_logo.png similarity index 100% rename from logo/sodexo_logo.png rename to Darwin/Assets.xcassets/sodexo_logo.imageset/sodexo_logo.png diff --git a/pedi-foods/Darwin/Assets.xcassets/tracking-canceled.imageset/Contents.json b/Darwin/Assets.xcassets/tracking-canceled.imageset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/tracking-canceled.imageset/Contents.json rename to Darwin/Assets.xcassets/tracking-canceled.imageset/Contents.json diff --git a/pedi-foods/Darwin/Assets.xcassets/tracking-canceled.imageset/tracking-canceled.png b/Darwin/Assets.xcassets/tracking-canceled.imageset/tracking-canceled.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/tracking-canceled.imageset/tracking-canceled.png rename to Darwin/Assets.xcassets/tracking-canceled.imageset/tracking-canceled.png diff --git a/pedi-foods/Darwin/Assets.xcassets/tracking-completed.imageset/Contents.json b/Darwin/Assets.xcassets/tracking-completed.imageset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/tracking-completed.imageset/Contents.json rename to Darwin/Assets.xcassets/tracking-completed.imageset/Contents.json diff --git a/pedi-foods/Darwin/Assets.xcassets/tracking-completed.imageset/tracking-completed.png b/Darwin/Assets.xcassets/tracking-completed.imageset/tracking-completed.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/tracking-completed.imageset/tracking-completed.png rename to Darwin/Assets.xcassets/tracking-completed.imageset/tracking-completed.png diff --git a/pedi-foods/Darwin/Assets.xcassets/tracking-delivering.imageset/Contents.json b/Darwin/Assets.xcassets/tracking-delivering.imageset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/tracking-delivering.imageset/Contents.json rename to Darwin/Assets.xcassets/tracking-delivering.imageset/Contents.json diff --git a/pedi-foods/Darwin/Assets.xcassets/tracking-delivering.imageset/tracking-delivering.png b/Darwin/Assets.xcassets/tracking-delivering.imageset/tracking-delivering.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/tracking-delivering.imageset/tracking-delivering.png rename to Darwin/Assets.xcassets/tracking-delivering.imageset/tracking-delivering.png diff --git a/pedi-foods/Darwin/Assets.xcassets/tracking-pending.imageset/Contents.json b/Darwin/Assets.xcassets/tracking-pending.imageset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/tracking-pending.imageset/Contents.json rename to Darwin/Assets.xcassets/tracking-pending.imageset/Contents.json diff --git a/pedi-foods/Darwin/Assets.xcassets/tracking-pending.imageset/tracking-pending.png b/Darwin/Assets.xcassets/tracking-pending.imageset/tracking-pending.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/tracking-pending.imageset/tracking-pending.png rename to Darwin/Assets.xcassets/tracking-pending.imageset/tracking-pending.png diff --git a/pedi-foods/Darwin/Assets.xcassets/tracking-preparing.imageset/Contents.json b/Darwin/Assets.xcassets/tracking-preparing.imageset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/tracking-preparing.imageset/Contents.json rename to Darwin/Assets.xcassets/tracking-preparing.imageset/Contents.json diff --git a/pedi-foods/Darwin/Assets.xcassets/tracking-preparing.imageset/tracking-preparing.png b/Darwin/Assets.xcassets/tracking-preparing.imageset/tracking-preparing.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/tracking-preparing.imageset/tracking-preparing.png rename to Darwin/Assets.xcassets/tracking-preparing.imageset/tracking-preparing.png diff --git a/pedi-foods/Darwin/Assets.xcassets/tracking-ready.imageset/Contents.json b/Darwin/Assets.xcassets/tracking-ready.imageset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/tracking-ready.imageset/Contents.json rename to Darwin/Assets.xcassets/tracking-ready.imageset/Contents.json diff --git a/pedi-foods/Darwin/Assets.xcassets/tracking-ready.imageset/tracking-ready.png b/Darwin/Assets.xcassets/tracking-ready.imageset/tracking-ready.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/tracking-ready.imageset/tracking-ready.png rename to Darwin/Assets.xcassets/tracking-ready.imageset/tracking-ready.png diff --git a/pedi-foods/Darwin/Assets.xcassets/visacard_logo.imageset/Contents.json b/Darwin/Assets.xcassets/visacard_logo.imageset/Contents.json similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/visacard_logo.imageset/Contents.json rename to Darwin/Assets.xcassets/visacard_logo.imageset/Contents.json diff --git a/logo/visacard_logo.png b/Darwin/Assets.xcassets/visacard_logo.imageset/visacard_logo.png similarity index 100% rename from logo/visacard_logo.png rename to Darwin/Assets.xcassets/visacard_logo.imageset/visacard_logo.png diff --git a/pedi-foods/Darwin/Entitlements.plist b/Darwin/Entitlements.plist similarity index 100% rename from pedi-foods/Darwin/Entitlements.plist rename to Darwin/Entitlements.plist diff --git a/pedi-foods/Darwin/Info.plist b/Darwin/Info.plist similarity index 100% rename from pedi-foods/Darwin/Info.plist rename to Darwin/Info.plist diff --git a/pedi-foods/Darwin/PediFoods.xcconfig b/Darwin/PediFoods.xcconfig similarity index 100% rename from pedi-foods/Darwin/PediFoods.xcconfig rename to Darwin/PediFoods.xcconfig diff --git a/pedi-foods/Darwin/PediFoods.xcodeproj/project.pbxproj b/Darwin/PediFoods.xcodeproj/project.pbxproj similarity index 97% rename from pedi-foods/Darwin/PediFoods.xcodeproj/project.pbxproj rename to Darwin/PediFoods.xcodeproj/project.pbxproj index fd9fbd3..7b8fcbe 100644 --- a/pedi-foods/Darwin/PediFoods.xcodeproj/project.pbxproj +++ b/Darwin/PediFoods.xcodeproj/project.pbxproj @@ -185,6 +185,9 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; @@ -199,6 +202,9 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; @@ -267,7 +273,7 @@ repositoryURL = "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.10; + minimumVersion = 1.0.11; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/pedi-foods/Darwin/PediFoods.xcodeproj/xcshareddata/xcschemes/PediFoods App.xcscheme b/Darwin/PediFoods.xcodeproj/xcshareddata/xcschemes/PediFoods App.xcscheme similarity index 100% rename from pedi-foods/Darwin/PediFoods.xcodeproj/xcshareddata/xcschemes/PediFoods App.xcscheme rename to Darwin/PediFoods.xcodeproj/xcshareddata/xcschemes/PediFoods App.xcscheme diff --git a/pedi-foods/Darwin/Sources/LaunchScreen.storyboard b/Darwin/Sources/LaunchScreen.storyboard similarity index 100% rename from pedi-foods/Darwin/Sources/LaunchScreen.storyboard rename to Darwin/Sources/LaunchScreen.storyboard diff --git a/pedi-foods/Darwin/Sources/Main.swift b/Darwin/Sources/Main.swift similarity index 100% rename from pedi-foods/Darwin/Sources/Main.swift rename to Darwin/Sources/Main.swift diff --git a/pedi-foods/Darwin/fastlane/AppStore.xcconfig b/Darwin/fastlane/AppStore.xcconfig similarity index 100% rename from pedi-foods/Darwin/fastlane/AppStore.xcconfig rename to Darwin/fastlane/AppStore.xcconfig diff --git a/pedi-foods/Darwin/fastlane/Appfile b/Darwin/fastlane/Appfile similarity index 100% rename from pedi-foods/Darwin/fastlane/Appfile rename to Darwin/fastlane/Appfile diff --git a/pedi-foods/Darwin/fastlane/Deliverfile b/Darwin/fastlane/Deliverfile similarity index 100% rename from pedi-foods/Darwin/fastlane/Deliverfile rename to Darwin/fastlane/Deliverfile diff --git a/pedi-foods/Darwin/fastlane/Fastfile b/Darwin/fastlane/Fastfile similarity index 100% rename from pedi-foods/Darwin/fastlane/Fastfile rename to Darwin/fastlane/Fastfile diff --git a/pedi-foods/Darwin/fastlane/metadata/app_privacy_details.json b/Darwin/fastlane/metadata/app_privacy_details.json similarity index 100% rename from pedi-foods/Darwin/fastlane/metadata/app_privacy_details.json rename to Darwin/fastlane/metadata/app_privacy_details.json diff --git a/pedi-foods/Darwin/fastlane/metadata/en-US/description.txt b/Darwin/fastlane/metadata/en-US/description.txt similarity index 100% rename from pedi-foods/Darwin/fastlane/metadata/en-US/description.txt rename to Darwin/fastlane/metadata/en-US/description.txt diff --git a/pedi-foods/Darwin/fastlane/metadata/en-US/keywords.txt b/Darwin/fastlane/metadata/en-US/keywords.txt similarity index 100% rename from pedi-foods/Darwin/fastlane/metadata/en-US/keywords.txt rename to Darwin/fastlane/metadata/en-US/keywords.txt diff --git a/pedi-foods/Darwin/fastlane/metadata/en-US/privacy_url.txt b/Darwin/fastlane/metadata/en-US/privacy_url.txt similarity index 100% rename from pedi-foods/Darwin/fastlane/metadata/en-US/privacy_url.txt rename to Darwin/fastlane/metadata/en-US/privacy_url.txt diff --git a/pedi-foods/Darwin/fastlane/metadata/en-US/release_notes.txt b/Darwin/fastlane/metadata/en-US/release_notes.txt similarity index 100% rename from pedi-foods/Darwin/fastlane/metadata/en-US/release_notes.txt rename to Darwin/fastlane/metadata/en-US/release_notes.txt diff --git a/pedi-foods/Darwin/fastlane/metadata/en-US/software_url.txt b/Darwin/fastlane/metadata/en-US/software_url.txt similarity index 100% rename from pedi-foods/Darwin/fastlane/metadata/en-US/software_url.txt rename to Darwin/fastlane/metadata/en-US/software_url.txt diff --git a/pedi-foods/Darwin/fastlane/metadata/en-US/subtitle.txt b/Darwin/fastlane/metadata/en-US/subtitle.txt similarity index 100% rename from pedi-foods/Darwin/fastlane/metadata/en-US/subtitle.txt rename to Darwin/fastlane/metadata/en-US/subtitle.txt diff --git a/pedi-foods/Darwin/fastlane/metadata/en-US/support_url.txt b/Darwin/fastlane/metadata/en-US/support_url.txt similarity index 100% rename from pedi-foods/Darwin/fastlane/metadata/en-US/support_url.txt rename to Darwin/fastlane/metadata/en-US/support_url.txt diff --git a/pedi-foods/Android/fastlane/metadata/android/en-US/title.txt b/Darwin/fastlane/metadata/en-US/title.txt similarity index 100% rename from pedi-foods/Android/fastlane/metadata/android/en-US/title.txt rename to Darwin/fastlane/metadata/en-US/title.txt diff --git a/pedi-foods/Darwin/fastlane/metadata/en-US/version_whats_new.txt b/Darwin/fastlane/metadata/en-US/version_whats_new.txt similarity index 100% rename from pedi-foods/Darwin/fastlane/metadata/en-US/version_whats_new.txt rename to Darwin/fastlane/metadata/en-US/version_whats_new.txt diff --git a/pedi-foods/Darwin/fastlane/metadata/rating.json b/Darwin/fastlane/metadata/rating.json similarity index 100% rename from pedi-foods/Darwin/fastlane/metadata/rating.json rename to Darwin/fastlane/metadata/rating.json diff --git a/pedi-foods/PROFILE_PLAN.md b/PROFILE_PLAN.md similarity index 100% rename from pedi-foods/PROFILE_PLAN.md rename to PROFILE_PLAN.md diff --git a/pedi-foods/PROJECT_ANALYSIS_SKIP.md b/PROJECT_ANALYSIS_SKIP.md similarity index 100% rename from pedi-foods/PROJECT_ANALYSIS_SKIP.md rename to PROJECT_ANALYSIS_SKIP.md diff --git a/pedi-foods/Package.swift b/Package.swift similarity index 100% rename from pedi-foods/Package.swift rename to Package.swift diff --git a/PediFoods.code-workspace b/PediFoods.code-workspace deleted file mode 100644 index 876a149..0000000 --- a/PediFoods.code-workspace +++ /dev/null @@ -1,8 +0,0 @@ -{ - "folders": [ - { - "path": "." - } - ], - "settings": {} -} \ No newline at end of file diff --git a/pedi-foods/Project.xcworkspace/contents.xcworkspacedata b/Project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from pedi-foods/Project.xcworkspace/contents.xcworkspacedata rename to Project.xcworkspace/contents.xcworkspacedata diff --git a/README.md b/README.md index 89707cb..c663e6a 100644 --- a/README.md +++ b/README.md @@ -1,99 +1,62 @@ -# ⚠️ INFORMAÇÕES CONFIDENCIAIS E PROPRIEDADE INTELECTUAL ⚠️ +# PediFoods -## 🚨 AVISO LEGAL IMPORTANTE +This is a [Skip](https://skip.tools) dual-platform app project. -**ATENÇÃO: Este repositório contém informações CONFIDENCIAIS e PROPRIETÁRIAS.** -### ⛔ PROIBIÇÕES ABSOLUTAS + -### 🔒 DIREITOS DE PROPRIEDADE INTELECTUAL + -- **Lei de Propriedade Intelectual (Lei 9.610/98)** -- **Lei de Software (Lei 9.609/98)** -- **Lei Geral de Proteção de Dados (LGPD - Lei 13.709/18)** -- **Direitos Autorais e Patentes** +## Building -### ⚖️ CONSEQUÊNCIAS LEGAIS +This project is both a stand-alone Swift Package Manager module, +as well as an Xcode project that builds and translates the project +into a Kotlin Gradle project for Android using the skipstone plugin. -O acesso não autorizado, uso indevido, cópia ou distribuição deste código constitui: +Building the module requires that Skip be installed using +[Homebrew](https://brew.sh) with `brew install skiptools/skip/skip`. -- **CRIME** de violação de propriedade intelectual -- **CRIME** de roubo de propriedade intelectual -- **CRIME** de violação de direitos autorais -- **CRIME** de acesso não autorizado a sistema informatizado +This will also install the necessary Skip prerequisites: +Kotlin, Gradle, and the Android build tools. -**Penas previstas: 2 a 5 anos de reclusão + multa** +Installation prerequisites can be confirmed by running +`skip checkup`. The project can be validated with `skip verify`. -### 🛡️ MEDIDAS DE SEGURANÇA +## Running -- **Monitoramento**: Todo acesso é registrado e monitorado -- **Rastreamento**: IPs e atividades são rastreados -- **Logs**: Sistema completo de auditoria -- **Criptografia**: Dados protegidos com AES-256 -- **Autenticação**: Acesso restrito com JWT + OTP +Xcode and Android Studio must be downloaded and installed in order to +run the app in the iOS simulator / Android emulator. +An Android emulator must already be running, which can be launched from +Android Studio's Device Manager. -### 📧 CONTATO AUTORIZADO +The project can be opened and run in Xcode from +`Project.xcworkspace`, which also enabled parallel +development of any Skip libary dependencies. -**ÚNICO CONTATO VÁLIDO:** -- **Email**: daniel@loverde.com.br -- **Autorização**: Apenas via email oficial +To run both the Swift and Kotlin apps simultaneously, +launch the "PediFoods App" target from Xcode. +A build phases runs the "Launch Android APK" script that +will deploy the Skip app to a running Android emulator or connected device. +Logging output for the iOS app can be viewed in the Xcode console, and in +Android Studio's logcat tab for the transpiled Kotlin app, or +using `adb logcat` from a terminal. -### 🔐 CLASSIFICAÇÃO +## Testing -**NÍVEL DE CONFIDENCIALIDADE: MÁXIMO** -- **Restrito**: Apenas para o proprietário -- **Não Divulgar**: Informações proprietárias -- **Não Compartilhar**: Código fonte protegido +The module can be tested using the standard `swift test` command +or by running the test target for the macOS destination in Xcode, +which will run the Swift tests as well as the transpiled +Kotlin JUnit tests in the Robolectric Android simulation environment. ---- - -## ⚠️ DISCLAIMER FINAL - -**Ao acessar este repositório, você concorda que:** - -1. **Reconhece** que este é um software proprietário -2. **Compromete-se** a não violar os direitos de propriedade intelectual -3. **Aceita** as consequências legais por qualquer violação -4. **Entende** que o acesso é monitorado e rastreado -5. **Concorda** em respeitar todas as leis aplicáveis - ---- - -## Sobre este repositório - -App **PediFoods iOS** — projeto [Skip](https://skip.tools) dual-platform em -Swift/SwiftUI (`pedi-foods/`), transpilado para Android via skipstone. É o app -de produção e a **fonte da verdade** para requests, responses e comportamento; -o port nativo Android é construído em paridade com ele. - -Build e execução: ver `pedi-foods/README.md` (Skip, `skip checkup`/`skip verify`, -Xcode + emulador Android). Testes via `swift test` / `skip test`. - ---- - -## Fluxo Git (desenvolvimento) - -- Ao terminar cada task ou fatia lógica (ex.: fatia de checkout, ajuste de tela, correção de testes): fazer **commit imediato** com mensagem clara (`feat:`, `fix:`, `chore:`). -- Não commitar artefactos de build (`.build/`, saída do skipstone, `Darwin/` gerado) nem ficheiros de estado local irrelevantes para o projeto rodar a partir do código-fonte, salvo pedido explícito. -- O agente faz commit local; o push é sempre feito pelo Daniel após revisão. - ---- - -**© 2024 Daniel Arantes Loverde. Todos os direitos reservados.** - -**Última atualização: Junho 2026** - ---- - -*Este documento é parte integrante do software e deve ser mantido confidencial.* +Parity testing can be performed with `skip test`, +which will output a table of the test results for both platforms. Autor: Daniel Arantes Loverde diff --git a/pedi-foods/Skip.env b/Skip.env similarity index 100% rename from pedi-foods/Skip.env rename to Skip.env diff --git a/pedi-foods/Sources/PediFoods/Components/Buttons.swift b/Sources/PediFoods/Components/Buttons.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Components/Buttons.swift rename to Sources/PediFoods/Components/Buttons.swift diff --git a/pedi-foods/Sources/PediFoods/Components/CachedRemoteImage.swift b/Sources/PediFoods/Components/CachedRemoteImage.swift similarity index 65% rename from pedi-foods/Sources/PediFoods/Components/CachedRemoteImage.swift rename to Sources/PediFoods/Components/CachedRemoteImage.swift index 8b643b2..6a1461d 100644 --- a/pedi-foods/Sources/PediFoods/Components/CachedRemoteImage.swift +++ b/Sources/PediFoods/Components/CachedRemoteImage.swift @@ -6,9 +6,20 @@ import UIKit import AppKit #endif +enum ImageFitMode { + /// Scale to cover the whole box, cropping whichever axis overflows. + /// Default — matches historical behavior for every image in the app. + case fill + /// Scale so height always matches the box exactly; width follows the + /// source's aspect ratio and gets cropped/gapped on the sides. Opt-in + /// only — used by the Store Detail cover header. + case heightFit +} + struct CachedRemoteImage: View { let imageURL: String? let ttl: TimeInterval + let fitMode: ImageFitMode let placeholder: Placeholder #if canImport(UIKit) || canImport(AppKit) @@ -18,48 +29,50 @@ struct CachedRemoteImage: View { init( imageURL: String?, ttl: TimeInterval = AppCacheTTL.twoHours, + fitMode: ImageFitMode = .fill, @ViewBuilder placeholder: () -> Placeholder ) { self.imageURL = imageURL self.ttl = ttl + self.fitMode = fitMode self.placeholder = placeholder() } var body: some View { - Group { + GeometryReader { geometry in + Group { #if canImport(UIKit) - if let uiImage = loader.uiImage { - Image(uiImage: uiImage) - .resizable() - .scaledToFill() - } else { - placeholder - } -#elseif canImport(AppKit) - if let nsImage = loader.nsImage { - Image(nsImage: nsImage) - .resizable() - .scaledToFill() - } else { - placeholder - } -#else - if let imageURL, - let url = URL(string: imageURL) { - AsyncImage(url: url) { phase in - switch phase { - case .success(let image): - image - .resizable() - .scaledToFill() - default: - placeholder - } + if let uiImage = loader.uiImage { + rendered(image: Image(uiImage: uiImage), pixelSize: uiImage.size, in: geometry.size) + } else { + placeholder + } +#elseif canImport(AppKit) + if let nsImage = loader.nsImage { + rendered(image: Image(nsImage: nsImage), pixelSize: nsImage.size, in: geometry.size) + } else { + placeholder + } +#else + if let imageURL, + let url = URL(string: imageURL) { + AsyncImage(url: url) { phase in + switch phase { + case .success(let image): + image + .resizable() + .scaledToFill() + default: + placeholder + } + } + } else { + placeholder } - } else { - placeholder - } #endif + } + .frame(width: geometry.size.width, height: geometry.size.height) + .clipped() } .onAppear { #if canImport(UIKit) || canImport(AppKit) @@ -72,6 +85,25 @@ struct CachedRemoteImage: View { #endif } } + +#if canImport(UIKit) || canImport(AppKit) + @ViewBuilder + private func rendered(image: Image, pixelSize: CGSize, in containerSize: CGSize) -> some View { + switch fitMode { + case .fill: + image + .resizable() + .scaledToFill() + case .heightFit: + let aspect = pixelSize.height > 0 ? pixelSize.width / pixelSize.height : 1 + let renderWidth = containerSize.height * aspect + image + .resizable() + .frame(width: renderWidth, height: containerSize.height) + .frame(width: containerSize.width, height: containerSize.height) + } + } +#endif } #if canImport(UIKit) || canImport(AppKit) diff --git a/pedi-foods/Sources/PediFoods/Components/Inputs.swift b/Sources/PediFoods/Components/Inputs.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Components/Inputs.swift rename to Sources/PediFoods/Components/Inputs.swift diff --git a/pedi-foods/Sources/PediFoods/Components/SnackbarOverlay.swift b/Sources/PediFoods/Components/SnackbarOverlay.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Components/SnackbarOverlay.swift rename to Sources/PediFoods/Components/SnackbarOverlay.swift diff --git a/pedi-foods/Sources/PediFoods/Components/StoreCard.swift b/Sources/PediFoods/Components/StoreCard.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Components/StoreCard.swift rename to Sources/PediFoods/Components/StoreCard.swift diff --git a/pedi-foods/Sources/PediFoods/Components/TextFieldCompat.swift b/Sources/PediFoods/Components/TextFieldCompat.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Components/TextFieldCompat.swift rename to Sources/PediFoods/Components/TextFieldCompat.swift diff --git a/pedi-foods/Sources/PediFoods/ContentView.swift b/Sources/PediFoods/ContentView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/ContentView.swift rename to Sources/PediFoods/ContentView.swift diff --git a/pedi-foods/Sources/PediFoods/Coordinators/AppCoordinator.swift b/Sources/PediFoods/Coordinators/AppCoordinator.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Coordinators/AppCoordinator.swift rename to Sources/PediFoods/Coordinators/AppCoordinator.swift diff --git a/pedi-foods/Sources/PediFoods/DesignSystem/DesignSystem.swift b/Sources/PediFoods/DesignSystem/DesignSystem.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/DesignSystem/DesignSystem.swift rename to Sources/PediFoods/DesignSystem/DesignSystem.swift diff --git a/pedi-foods/Sources/PediFoods/PediFoodsApp.swift b/Sources/PediFoods/PediFoodsApp.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/PediFoodsApp.swift rename to Sources/PediFoods/PediFoodsApp.swift diff --git a/pedi-foods/Sources/PediFoods/Resources/Localizable.xcstrings b/Sources/PediFoods/Resources/Localizable.xcstrings similarity index 99% rename from pedi-foods/Sources/PediFoods/Resources/Localizable.xcstrings rename to Sources/PediFoods/Resources/Localizable.xcstrings index a3deaf9..1680d5b 100644 --- a/pedi-foods/Sources/PediFoods/Resources/Localizable.xcstrings +++ b/Sources/PediFoods/Resources/Localizable.xcstrings @@ -215,6 +215,9 @@ }, "Atualizando status do pedido..." : { + }, + "Atualizando..." : { + }, "Avaliar Pedido" : { @@ -825,9 +828,6 @@ "O que vai querer \npedir hoje?" : { "comment" : "A title displayed above the search bar in the home view.", "isCommentAutoGenerated" : true - }, - "O que vai querer pedir hoje?\n " : { - }, "O que você achou do pedido?" : { "comment" : "A question prompt asking users to rate their experience with the order.", diff --git a/pedi-foods/Sources/PediFoods/Resources/Module.xcassets/Contents.json b/Sources/PediFoods/Resources/Module.xcassets/Contents.json similarity index 100% rename from pedi-foods/Sources/PediFoods/Resources/Module.xcassets/Contents.json rename to Sources/PediFoods/Resources/Module.xcassets/Contents.json diff --git a/pedi-foods/Darwin/Assets.xcassets/alelocard_logo.imageset/alelocard_logo.png b/Sources/PediFoods/Resources/alelocard_logo.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/alelocard_logo.imageset/alelocard_logo.png rename to Sources/PediFoods/Resources/alelocard_logo.png diff --git a/pedi-foods/Darwin/Assets.xcassets/amexcard_logo.imageset/amexcard_logo.png b/Sources/PediFoods/Resources/amexcard_logo.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/amexcard_logo.imageset/amexcard_logo.png rename to Sources/PediFoods/Resources/amexcard_logo.png diff --git a/pedi-foods/Darwin/Assets.xcassets/hipercard_logo.imageset/hipercard_logo.png b/Sources/PediFoods/Resources/hipercard_logo.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/hipercard_logo.imageset/hipercard_logo.png rename to Sources/PediFoods/Resources/hipercard_logo.png diff --git a/pedi-foods/Darwin/Assets.xcassets/icon_pix.imageset/icon_pix.png b/Sources/PediFoods/Resources/icon_pix.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/icon_pix.imageset/icon_pix.png rename to Sources/PediFoods/Resources/icon_pix.png diff --git a/pedi-foods/Android/app/src/main/res/drawable/login_dark.png b/Sources/PediFoods/Resources/login_dark.png similarity index 100% rename from pedi-foods/Android/app/src/main/res/drawable/login_dark.png rename to Sources/PediFoods/Resources/login_dark.png diff --git a/pedi-foods/Android/app/src/main/res/drawable/login_light.png b/Sources/PediFoods/Resources/login_light.png similarity index 100% rename from pedi-foods/Android/app/src/main/res/drawable/login_light.png rename to Sources/PediFoods/Resources/login_light.png diff --git a/pedi-foods/Darwin/Assets.xcassets/mastercard_logo.imageset/mastercard_logo.png b/Sources/PediFoods/Resources/mastercard_logo.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/mastercard_logo.imageset/mastercard_logo.png rename to Sources/PediFoods/Resources/mastercard_logo.png diff --git a/pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/Assets/Screenshot 2026-04-25 at 16.22.36.png b/Sources/PediFoods/Resources/pedifoods.icon/Assets/Screenshot 2026-04-25 at 16.22.36.png similarity index 100% rename from pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/Assets/Screenshot 2026-04-25 at 16.22.36.png rename to Sources/PediFoods/Resources/pedifoods.icon/Assets/Screenshot 2026-04-25 at 16.22.36.png diff --git a/pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/Assets/Screenshot 2026-04-25 at 16.22.59.png b/Sources/PediFoods/Resources/pedifoods.icon/Assets/Screenshot 2026-04-25 at 16.22.59.png similarity index 100% rename from pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/Assets/Screenshot 2026-04-25 at 16.22.59.png rename to Sources/PediFoods/Resources/pedifoods.icon/Assets/Screenshot 2026-04-25 at 16.22.59.png diff --git a/pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/icon.json b/Sources/PediFoods/Resources/pedifoods.icon/icon.json similarity index 100% rename from pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/icon.json rename to Sources/PediFoods/Resources/pedifoods.icon/icon.json diff --git a/pedi-foods/Android/app/src/main/res/drawable/pedifoods.png b/Sources/PediFoods/Resources/pedifoods.png similarity index 100% rename from pedi-foods/Android/app/src/main/res/drawable/pedifoods.png rename to Sources/PediFoods/Resources/pedifoods.png diff --git a/pedi-foods/Android/app/src/main/res/drawable/pedifoods_dark.png b/Sources/PediFoods/Resources/pedifoods_dark.png similarity index 100% rename from pedi-foods/Android/app/src/main/res/drawable/pedifoods_dark.png rename to Sources/PediFoods/Resources/pedifoods_dark.png diff --git a/pedi-foods/Darwin/Assets.xcassets/pin_image_app.imageset/pin_image_app.png b/Sources/PediFoods/Resources/pin_image_app.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/pin_image_app.imageset/pin_image_app.png rename to Sources/PediFoods/Resources/pin_image_app.png diff --git a/pedi-foods/Darwin/Assets.xcassets/pin_image_app.imageset/pin_image_app_dark.png b/Sources/PediFoods/Resources/pin_image_app_dark.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/pin_image_app.imageset/pin_image_app_dark.png rename to Sources/PediFoods/Resources/pin_image_app_dark.png diff --git a/pedi-foods/Sources/PediFoods/Resources/placeholder-pizza.png b/Sources/PediFoods/Resources/placeholder-pizza.png similarity index 100% rename from pedi-foods/Sources/PediFoods/Resources/placeholder-pizza.png rename to Sources/PediFoods/Resources/placeholder-pizza.png diff --git a/pedi-foods/Sources/PediFoods/Resources/placeholder-product.png b/Sources/PediFoods/Resources/placeholder-product.png similarity index 100% rename from pedi-foods/Sources/PediFoods/Resources/placeholder-product.png rename to Sources/PediFoods/Resources/placeholder-product.png diff --git a/pedi-foods/Darwin/Assets.xcassets/sodexo_logo.imageset/sodexo_logo.png b/Sources/PediFoods/Resources/sodexo_logo.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/sodexo_logo.imageset/sodexo_logo.png rename to Sources/PediFoods/Resources/sodexo_logo.png diff --git a/pedi-foods/Sources/PediFoods/Resources/tracking-canceled.png b/Sources/PediFoods/Resources/tracking-canceled.png similarity index 100% rename from pedi-foods/Sources/PediFoods/Resources/tracking-canceled.png rename to Sources/PediFoods/Resources/tracking-canceled.png diff --git a/pedi-foods/Sources/PediFoods/Resources/tracking-completed.png b/Sources/PediFoods/Resources/tracking-completed.png similarity index 100% rename from pedi-foods/Sources/PediFoods/Resources/tracking-completed.png rename to Sources/PediFoods/Resources/tracking-completed.png diff --git a/pedi-foods/Sources/PediFoods/Resources/tracking-delivering.png b/Sources/PediFoods/Resources/tracking-delivering.png similarity index 100% rename from pedi-foods/Sources/PediFoods/Resources/tracking-delivering.png rename to Sources/PediFoods/Resources/tracking-delivering.png diff --git a/pedi-foods/Sources/PediFoods/Resources/tracking-pending.png b/Sources/PediFoods/Resources/tracking-pending.png similarity index 100% rename from pedi-foods/Sources/PediFoods/Resources/tracking-pending.png rename to Sources/PediFoods/Resources/tracking-pending.png diff --git a/pedi-foods/Sources/PediFoods/Resources/tracking-preparing.png b/Sources/PediFoods/Resources/tracking-preparing.png similarity index 100% rename from pedi-foods/Sources/PediFoods/Resources/tracking-preparing.png rename to Sources/PediFoods/Resources/tracking-preparing.png diff --git a/pedi-foods/Sources/PediFoods/Resources/tracking-ready.png b/Sources/PediFoods/Resources/tracking-ready.png similarity index 100% rename from pedi-foods/Sources/PediFoods/Resources/tracking-ready.png rename to Sources/PediFoods/Resources/tracking-ready.png diff --git a/pedi-foods/Darwin/Assets.xcassets/visacard_logo.imageset/visacard_logo.png b/Sources/PediFoods/Resources/visacard_logo.png similarity index 100% rename from pedi-foods/Darwin/Assets.xcassets/visacard_logo.imageset/visacard_logo.png rename to Sources/PediFoods/Resources/visacard_logo.png diff --git a/pedi-foods/Sources/PediFoods/Services/ApiCardModels.swift b/Sources/PediFoods/Services/ApiCardModels.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/ApiCardModels.swift rename to Sources/PediFoods/Services/ApiCardModels.swift diff --git a/pedi-foods/Sources/PediFoods/Services/ApiClient.swift b/Sources/PediFoods/Services/ApiClient.swift similarity index 92% rename from pedi-foods/Sources/PediFoods/Services/ApiClient.swift rename to Sources/PediFoods/Services/ApiClient.swift index f5cab1d..cdc919a 100644 --- a/pedi-foods/Sources/PediFoods/Services/ApiClient.swift +++ b/Sources/PediFoods/Services/ApiClient.swift @@ -14,6 +14,7 @@ enum NetworkError: Error, LocalizedError { case decodeError(String?) case rateLimited(Int?) case cancelled + case timedOut case transportError(String) var errorDescription: String? { @@ -36,6 +37,8 @@ enum NetworkError: Error, LocalizedError { return "Muitas requisicoes. Tente novamente." case .cancelled: return "Requisicao cancelada" + case .timedOut: + return "O servidor demorou demais para responder. Tente novamente." case .transportError(let message): return "Erro de rede: \(message)" } @@ -70,7 +73,7 @@ struct ApiRequest: Sendable { } } -final class ApiClient { +final class ApiClient: @unchecked Sendable { private let session: URLSession private let tokenStore: TokenStore private let maxAttempts = 3 @@ -81,12 +84,36 @@ final class ApiClient { self.tokenStore = tokenStore } - func send(_ request: ApiRequest) async throws -> T { + func send(_ request: ApiRequest) async throws -> T { + // Hard client-side cutoff independent of whatever timeout logic + // lives inside the underlying transport (LCEssentials or plain + // URLSession). If that transport ever stalls without ever + // resolving — no response, no error, nothing — the UI must still + // get an answer so it can stop showing "nothing happened." + try await withTimeout(seconds: 20) { [self] in #if canImport(LCEssentials) && os(iOS) - return try await sendWithLCEssentials(request) + try await sendWithLCEssentials(request) #else - return try await sendWithURLSession(request) + try await sendWithURLSession(request) #endif + } + } + + private func withTimeout(seconds: Double, operation: @escaping @Sendable () async throws -> T) async throws -> T { + try await withThrowingTaskGroup(of: T.self) { group in + group.addTask { + try await operation() + } + group.addTask { + try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) + throw NetworkError.timedOut + } + guard let result = try await group.next() else { + throw NetworkError.timedOut + } + group.cancelAll() + return result + } } } @@ -299,7 +326,7 @@ private extension ApiClient { return true case .httpError(let statusCode, _): return statusCode >= 500 - case .invalidURL, .invalidResponse, .decodeError, .unauthorized, .cancelled: + case .invalidURL, .invalidResponse, .decodeError, .unauthorized, .cancelled, .timedOut: return false } } diff --git a/pedi-foods/Sources/PediFoods/Services/ApiConfig.swift b/Sources/PediFoods/Services/ApiConfig.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/ApiConfig.swift rename to Sources/PediFoods/Services/ApiConfig.swift diff --git a/pedi-foods/Sources/PediFoods/Services/ApiCustomerPayloadModels.swift b/Sources/PediFoods/Services/ApiCustomerPayloadModels.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/ApiCustomerPayloadModels.swift rename to Sources/PediFoods/Services/ApiCustomerPayloadModels.swift diff --git a/pedi-foods/Sources/PediFoods/Services/ApiFavoriteModels.swift b/Sources/PediFoods/Services/ApiFavoriteModels.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/ApiFavoriteModels.swift rename to Sources/PediFoods/Services/ApiFavoriteModels.swift diff --git a/pedi-foods/Sources/PediFoods/Services/ApiModels.swift b/Sources/PediFoods/Services/ApiModels.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/ApiModels.swift rename to Sources/PediFoods/Services/ApiModels.swift diff --git a/pedi-foods/Sources/PediFoods/Services/ApiOrderModels.swift b/Sources/PediFoods/Services/ApiOrderModels.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/ApiOrderModels.swift rename to Sources/PediFoods/Services/ApiOrderModels.swift diff --git a/pedi-foods/Sources/PediFoods/Services/ApiOrderTrackingModels.swift b/Sources/PediFoods/Services/ApiOrderTrackingModels.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/ApiOrderTrackingModels.swift rename to Sources/PediFoods/Services/ApiOrderTrackingModels.swift diff --git a/pedi-foods/Sources/PediFoods/Services/ApiPizzaModels.swift b/Sources/PediFoods/Services/ApiPizzaModels.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/ApiPizzaModels.swift rename to Sources/PediFoods/Services/ApiPizzaModels.swift diff --git a/pedi-foods/Sources/PediFoods/Services/ApiReviewModels.swift b/Sources/PediFoods/Services/ApiReviewModels.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/ApiReviewModels.swift rename to Sources/PediFoods/Services/ApiReviewModels.swift diff --git a/pedi-foods/Sources/PediFoods/Services/ApiService.swift b/Sources/PediFoods/Services/ApiService.swift similarity index 99% rename from pedi-foods/Sources/PediFoods/Services/ApiService.swift rename to Sources/PediFoods/Services/ApiService.swift index 4eb13a7..5d55a62 100644 --- a/pedi-foods/Sources/PediFoods/Services/ApiService.swift +++ b/Sources/PediFoods/Services/ApiService.swift @@ -11,7 +11,7 @@ enum ApiServiceError: Error, LocalizedError { } } -struct ApiEnvelope: Decodable { +struct ApiEnvelope: Decodable, Sendable { let error: Bool let code: String? let message: String? @@ -31,7 +31,7 @@ final class ApiService { self.tokenStore = tokenStore } - private func send(_ req: ApiRequest) async throws -> T { + private func send(_ req: ApiRequest) async throws -> T { do { return try await client.send(req) } catch let error as NetworkError { diff --git a/pedi-foods/Sources/PediFoods/Services/AppCache.swift b/Sources/PediFoods/Services/AppCache.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/AppCache.swift rename to Sources/PediFoods/Services/AppCache.swift diff --git a/pedi-foods/Sources/PediFoods/Services/FeatureControlService.swift b/Sources/PediFoods/Services/FeatureControlService.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/FeatureControlService.swift rename to Sources/PediFoods/Services/FeatureControlService.swift diff --git a/pedi-foods/Sources/PediFoods/Services/ImageSourceResolver.swift b/Sources/PediFoods/Services/ImageSourceResolver.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/ImageSourceResolver.swift rename to Sources/PediFoods/Services/ImageSourceResolver.swift diff --git a/pedi-foods/Sources/PediFoods/Services/LocationService.swift b/Sources/PediFoods/Services/LocationService.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/LocationService.swift rename to Sources/PediFoods/Services/LocationService.swift diff --git a/pedi-foods/Sources/PediFoods/Services/OrderRealtimeTracker.swift b/Sources/PediFoods/Services/OrderRealtimeTracker.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/OrderRealtimeTracker.swift rename to Sources/PediFoods/Services/OrderRealtimeTracker.swift diff --git a/pedi-foods/Sources/PediFoods/Services/SessionEvents.swift b/Sources/PediFoods/Services/SessionEvents.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/SessionEvents.swift rename to Sources/PediFoods/Services/SessionEvents.swift diff --git a/pedi-foods/Sources/PediFoods/Services/SessionStateStore.swift b/Sources/PediFoods/Services/SessionStateStore.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/SessionStateStore.swift rename to Sources/PediFoods/Services/SessionStateStore.swift diff --git a/pedi-foods/Sources/PediFoods/Services/StoreCatalogNormalizer.swift b/Sources/PediFoods/Services/StoreCatalogNormalizer.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/StoreCatalogNormalizer.swift rename to Sources/PediFoods/Services/StoreCatalogNormalizer.swift diff --git a/pedi-foods/Sources/PediFoods/Services/TokenStore.swift b/Sources/PediFoods/Services/TokenStore.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Services/TokenStore.swift rename to Sources/PediFoods/Services/TokenStore.swift diff --git a/pedi-foods/Sources/PediFoods/State/AppState.swift b/Sources/PediFoods/State/AppState.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/State/AppState.swift rename to Sources/PediFoods/State/AppState.swift diff --git a/pedi-foods/Sources/PediFoods/State/SnackbarCenter.swift b/Sources/PediFoods/State/SnackbarCenter.swift similarity index 84% rename from pedi-foods/Sources/PediFoods/State/SnackbarCenter.swift rename to Sources/PediFoods/State/SnackbarCenter.swift index ef52d17..8c7a9ca 100644 --- a/pedi-foods/Sources/PediFoods/State/SnackbarCenter.swift +++ b/Sources/PediFoods/State/SnackbarCenter.swift @@ -18,17 +18,22 @@ final class SnackbarCenter: ObservableObject { style: SnackbarStyle = .info, icon: String? = nil, duration: TimeInterval = 3.5, + isPersistent: Bool = false, action: (() -> Void)? = nil ) { dismissTask?.cancel() + dismissTask = nil current = SnackbarMessage( title: title, style: style, iconSystemName: icon, duration: duration, + isPersistent: isPersistent, action: action ) + guard isPersistent == false else { return } + dismissTask = Task { [weak self] in let nanos = UInt64(max(0.2, duration) * 1_000_000_000) try? await Task.sleep(nanoseconds: nanos) @@ -38,6 +43,7 @@ final class SnackbarCenter: ObservableObject { } func handleTap() { + guard current?.isPersistent != true else { return } let action = current?.action dismiss(animated: true) action?() @@ -54,6 +60,11 @@ final class SnackbarCenter: ObservableObject { current = nil } } + + func dismissPersistent() { + guard current?.isPersistent == true else { return } + dismiss(animated: true) + } } enum SnackbarStyle: Sendable { @@ -78,5 +89,6 @@ struct SnackbarMessage: Identifiable { let style: SnackbarStyle let iconSystemName: String? let duration: TimeInterval + let isPersistent: Bool let action: (() -> Void)? } diff --git a/pedi-foods/Sources/PediFoods/Support/PlatformCompat.swift b/Sources/PediFoods/Support/PlatformCompat.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Support/PlatformCompat.swift rename to Sources/PediFoods/Support/PlatformCompat.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Auth/AuthFlowView.swift b/Sources/PediFoods/Views/Auth/AuthFlowView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Auth/AuthFlowView.swift rename to Sources/PediFoods/Views/Auth/AuthFlowView.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Auth/AuthFormatters.swift b/Sources/PediFoods/Views/Auth/AuthFormatters.swift similarity index 96% rename from pedi-foods/Sources/PediFoods/Views/Auth/AuthFormatters.swift rename to Sources/PediFoods/Views/Auth/AuthFormatters.swift index cf300fa..728c933 100644 --- a/pedi-foods/Sources/PediFoods/Views/Auth/AuthFormatters.swift +++ b/Sources/PediFoods/Views/Auth/AuthFormatters.swift @@ -79,6 +79,8 @@ func userFacingAuthErrorMessage(_ error: Error) -> String { return "Ocorreu uma instabilidade ao processar sua solicitação. Tente novamente." case .cancelled: return "Cancelado" + case .timedOut: + return "O servidor demorou demais para responder. Tente novamente." } } diff --git a/pedi-foods/Sources/PediFoods/Views/Auth/LegalViews.swift b/Sources/PediFoods/Views/Auth/LegalViews.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Auth/LegalViews.swift rename to Sources/PediFoods/Views/Auth/LegalViews.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Auth/LoginEmailView.swift b/Sources/PediFoods/Views/Auth/LoginEmailView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Auth/LoginEmailView.swift rename to Sources/PediFoods/Views/Auth/LoginEmailView.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Auth/LoginView.swift b/Sources/PediFoods/Views/Auth/LoginView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Auth/LoginView.swift rename to Sources/PediFoods/Views/Auth/LoginView.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Auth/OtpView.swift b/Sources/PediFoods/Views/Auth/OtpView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Auth/OtpView.swift rename to Sources/PediFoods/Views/Auth/OtpView.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Auth/RegistrationView.swift b/Sources/PediFoods/Views/Auth/RegistrationView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Auth/RegistrationView.swift rename to Sources/PediFoods/Views/Auth/RegistrationView.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Launch/LaunchSplashView.swift b/Sources/PediFoods/Views/Launch/LaunchSplashView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Launch/LaunchSplashView.swift rename to Sources/PediFoods/Views/Launch/LaunchSplashView.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/AddAddressFormView.swift b/Sources/PediFoods/Views/Main/AddAddressFormView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/AddAddressFormView.swift rename to Sources/PediFoods/Views/Main/AddAddressFormView.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/AddCardFormView.swift b/Sources/PediFoods/Views/Main/AddCardFormView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/AddCardFormView.swift rename to Sources/PediFoods/Views/Main/AddCardFormView.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/AddressComponents.swift b/Sources/PediFoods/Views/Main/AddressComponents.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/AddressComponents.swift rename to Sources/PediFoods/Views/Main/AddressComponents.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/AddressesView.swift b/Sources/PediFoods/Views/Main/AddressesView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/AddressesView.swift rename to Sources/PediFoods/Views/Main/AddressesView.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/CartView.swift b/Sources/PediFoods/Views/Main/CartView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/CartView.swift rename to Sources/PediFoods/Views/Main/CartView.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/CheckoutTypes.swift b/Sources/PediFoods/Views/Main/CheckoutTypes.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/CheckoutTypes.swift rename to Sources/PediFoods/Views/Main/CheckoutTypes.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView+Logic.swift b/Sources/PediFoods/Views/Main/CheckoutView+Logic.swift similarity index 96% rename from pedi-foods/Sources/PediFoods/Views/Main/CheckoutView+Logic.swift rename to Sources/PediFoods/Views/Main/CheckoutView+Logic.swift index 438bd90..02fabab 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView+Logic.swift +++ b/Sources/PediFoods/Views/Main/CheckoutView+Logic.swift @@ -34,6 +34,21 @@ extension CheckoutView { String(format: "R$ %.2f", value).replacingOccurrences(of: ".", with: ",") } + @MainActor + func updateMinOrderSnackbar() { + guard isBelowMinOrder else { + SnackbarCenter.shared.dismissPersistent() + return + } + let missing = formatCurrency(minOrderValue - totalValue) + SnackbarCenter.shared.show( + title: "Pedido mínimo de \(formatCurrency(minOrderValue)). Faltam \(missing) para finalizar.", + style: .warning, + icon: "exclamationmark.circle.fill", + isPersistent: true + ) + } + @MainActor func loadStoreInfoIfNeeded() async { guard let storeId = appState.cart.storeId, storeId.isEmpty == false else { return } @@ -44,6 +59,9 @@ extension CheckoutView { return } storeInfo = response.result + if showPaymentModeToggle == false { + useInAppPayment = true + } errorMessage = nil } catch { errorMessage = "Não foi possível carregar opções de checkout." diff --git a/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView.swift b/Sources/PediFoods/Views/Main/CheckoutView.swift similarity index 96% rename from pedi-foods/Sources/PediFoods/Views/Main/CheckoutView.swift rename to Sources/PediFoods/Views/Main/CheckoutView.swift index 9180b4d..181023c 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView.swift +++ b/Sources/PediFoods/Views/Main/CheckoutView.swift @@ -58,6 +58,10 @@ struct CheckoutView: View { deliveryType == .delivery } + var showPaymentModeToggle: Bool { + availableStoreMachineMethods.isEmpty == false + } + private var deliveryToggle: Binding { Binding( get: { isDeliveryMode }, @@ -86,6 +90,14 @@ struct CheckoutView: View { AppColors.textMuted } + var minOrderValue: Double { + storeInfo?.minOrder ?? 0 + } + + var isBelowMinOrder: Bool { + minOrderValue > 0 && totalValue < minOrderValue + } + var canConfirmPayment: Bool { if isDeliveryMode { if addressValidationBlocked { return false } @@ -93,6 +105,7 @@ struct CheckoutView: View { if isValidatingAddress { return false } } if useInAppPayment == false && availableStoreMachineMethods.isEmpty { return false } + if isBelowMinOrder { return false } return true } @@ -132,6 +145,7 @@ struct CheckoutView: View { await refreshSelectedCustomerAddress() normalizeSelectedOptions() await validateDeliveryAddressIfNeeded() + updateMinOrderSnackbar() } .onChange(of: checkoutAddressWatchKey) { _, _ in if isRestoringAddress { return } @@ -144,6 +158,13 @@ struct CheckoutView: View { Task { await validateDeliveryAddressIfNeeded() } + updateMinOrderSnackbar() + } + .onChange(of: totalValue) { _, _ in + updateMinOrderSnackbar() + } + .onDisappear { + SnackbarCenter.shared.dismissPersistent() } .alert("Seu endereço não é atendido por esta loja.\nDeseja trocar mesmo assim?", isPresented: $showAddressNotServedAlert) { Button("Não", role: .cancel) { @@ -356,46 +377,48 @@ struct CheckoutView: View { .foregroundStyle(sectionTitleColor) VStack(alignment: .leading, spacing: 0) { - // Toggle row - HStack(spacing: 12) { - VStack(alignment: .leading, spacing: 2) { - Text(useInAppPayment ? "Pagar Pelo App" : "Pagar Na Maquininha") - .font(AppTypography.heading3) - .foregroundStyle(AppColors.textPrimary) - Text(useInAppPayment ? "Mais rápido e seguro" : "Na entrega/retirada com a loja") - .font(.caption) - .foregroundStyle(AppColors.textMuted) - } - Spacer() - Toggle("", isOn: Binding( - get: { useInAppPayment }, - set: { newValue in - withAnimation(.easeInOut(duration: 0.2)) { - useInAppPayment = newValue - if newValue { - if availableInAppPaymentMethods.contains(paymentMethod) == false { - paymentMethod = .pix - } - } else { - guard availableStoreMachineMethods.isEmpty == false else { - useInAppPayment = true - return - } - if availableStoreMachineMethods.contains(paymentMethod) == false, - let first = availableStoreMachineMethods.first { - paymentMethod = first + if showPaymentModeToggle { + // Toggle row + HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 2) { + Text(useInAppPayment ? "Pagar Pelo App" : "Pagar Na Maquininha") + .font(AppTypography.heading3) + .foregroundStyle(AppColors.textPrimary) + Text(useInAppPayment ? "Mais rápido e seguro" : "Na entrega/retirada com a loja") + .font(.caption) + .foregroundStyle(AppColors.textMuted) + } + Spacer() + Toggle("", isOn: Binding( + get: { useInAppPayment }, + set: { newValue in + withAnimation(.easeInOut(duration: 0.2)) { + useInAppPayment = newValue + if newValue { + if availableInAppPaymentMethods.contains(paymentMethod) == false { + paymentMethod = .pix + } + } else { + guard availableStoreMachineMethods.isEmpty == false else { + useInAppPayment = true + return + } + if availableStoreMachineMethods.contains(paymentMethod) == false, + let first = availableStoreMachineMethods.first { + paymentMethod = first + } } } } - } - )) - .labelsHidden() - .tint(AppColors.primary) - } - .padding(14) + )) + .labelsHidden() + .tint(AppColors.primary) + } + .padding(14) - Divider() - .padding(.horizontal, 14) + Divider() + .padding(.horizontal, 14) + } // Methods list if useInAppPayment { diff --git a/pedi-foods/Sources/PediFoods/Views/Main/FiltersModalView.swift b/Sources/PediFoods/Views/Main/FiltersModalView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/FiltersModalView.swift rename to Sources/PediFoods/Views/Main/FiltersModalView.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/HomeScrollOffsetObserver.swift b/Sources/PediFoods/Views/Main/HomeScrollOffsetObserver.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/HomeScrollOffsetObserver.swift rename to Sources/PediFoods/Views/Main/HomeScrollOffsetObserver.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/HomeView+Data.swift b/Sources/PediFoods/Views/Main/HomeView+Data.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/HomeView+Data.swift rename to Sources/PediFoods/Views/Main/HomeView+Data.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/HomeView+Favorites.swift b/Sources/PediFoods/Views/Main/HomeView+Favorites.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/HomeView+Favorites.swift rename to Sources/PediFoods/Views/Main/HomeView+Favorites.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/HomeView+Filtering.swift b/Sources/PediFoods/Views/Main/HomeView+Filtering.swift similarity index 99% rename from pedi-foods/Sources/PediFoods/Views/Main/HomeView+Filtering.swift rename to Sources/PediFoods/Views/Main/HomeView+Filtering.swift index 25682b5..7950f4b 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/HomeView+Filtering.swift +++ b/Sources/PediFoods/Views/Main/HomeView+Filtering.swift @@ -66,7 +66,7 @@ extension HomeView { promoText: nil, isFavorite: appState.favorites.storeIds.contains(store.id), iconName: "storefront", - imageURL: coverURL ?? logoURL, + imageURL: logoURL ?? coverURL, logoURL: logoURL, coverURL: coverURL, isOpen: store.isOpen ?? true, diff --git a/pedi-foods/Sources/PediFoods/Views/Main/HomeView.swift b/Sources/PediFoods/Views/Main/HomeView.swift similarity index 81% rename from pedi-foods/Sources/PediFoods/Views/Main/HomeView.swift rename to Sources/PediFoods/Views/Main/HomeView.swift index d4e8b34..7f4a23c 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/HomeView.swift +++ b/Sources/PediFoods/Views/Main/HomeView.swift @@ -42,19 +42,33 @@ struct HomeView: View { .padding(.bottom, contentBottomSpacing) } .refreshable { - await bootstrapStoresFlow( - forceNetworkRefresh: true, - category: selectedCategoryQueryValue, - refreshCategories: true - ) + // See StoreDetailView's .refreshable for why this runs in + // its own unstructured Task: SwiftUI can cancel + // .refreshable's own wrapping Task independent of whether + // the network call is still legitimately in flight, and + // that cancellation was being silently swallowed by + // isCancelledRequest — awaiting Task.value decouples the + // real work from that premature cancellation. + await Task { + await bootstrapStoresFlow( + forceNetworkRefresh: true, + category: selectedCategoryQueryValue, + refreshCategories: true + ) + }.value } .appNamedCoordinateSpace(HomeScrollCoordinateSpace.name) + // ignoresSafeArea lives here, on the header only — not on the + // .refreshable ScrollView above. Applying it to an ancestor of + // a .refreshable view (or the view itself) breaks the native + // pull-to-refresh spinner's positioning, rendering it invisible + // even though the gesture still fires the refresh closure. header(collapseProgress: collapseProgress, height: headerHeight) .frame(maxWidth: .infinity, alignment: .top) + .ignoresSafeArea(edges: .top) } .background(AppColors.backgroundLight) - .ignoresSafeArea(edges: .top) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .onAppear { if hasRequestedLocation == false { @@ -86,6 +100,22 @@ struct HomeView: View { private var contentStack: some View { VStack(spacing: 24) { scrollOffsetObserver + + // The collapsing header is a separate overlay drawn on top of + // this ScrollView in the ZStack above, which visually covers + // the native pull-to-refresh spinner's position. This gives + // refresh feedback that's actually visible, right below the + // header, instead of relying on a spinner hidden behind it. + if isLoadingStores && stores.isEmpty == false { + HStack(spacing: 8) { + ProgressView() + Text("Atualizando...") + .font(AppTypography.caption) + .foregroundStyle(AppColors.textMuted) + } + .frame(maxWidth: .infinity) + } + categoriesSection section(title: "Featured") { @@ -137,7 +167,13 @@ struct HomeView: View { } section(title: "Pertinho de você") { - if isLoadingStores { + // Once we have stores loaded, keep showing them regardless of + // a subsequent refresh's isLoadingStores/storesError state — + // a failed or in-flight pull-to-refresh must never hide + // already-loaded content. + if filteredStoreCards.isEmpty == false { + storeCardsList + } else if isLoadingStores { HStack { ProgressView() Text("Buscando estabelecimentos próximos...") @@ -164,45 +200,48 @@ struct HomeView: View { .foregroundStyle(AppColors.primary) } .padding(.horizontal, 20) - } else if filteredStoreCards.isEmpty { + } else { Text(emptyResultMessage) .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) .padding(.horizontal, 20) - } else { - VStack(spacing: 16) { - ForEach(filteredStoreCards) { store in - NavigationLink { - StoreDetailView( - storeId: store.id, - storeName: store.name, - storeCoverURL: store.coverURL, - storeLogoURL: store.logoURL, - storeCategory: store.category, - storeRating: store.rating, - storeDistance: store.distance, - storeDeliveryFee: store.deliveryFee, - appState: $appState - ) - } label: { - FeaturedStoreCard( - store: store, - onFavoriteToggle: { - Task { - await toggleFavoriteStore(storeId: store.id, storeName: store.name) - } - } - ) - } - .buttonStyle(.plain) - } - } - .padding(.horizontal, 20) } } } } + @ViewBuilder + private var storeCardsList: some View { + VStack(spacing: 16) { + ForEach(filteredStoreCards) { store in + NavigationLink { + StoreDetailView( + storeId: store.id, + storeName: store.name, + storeCoverURL: store.coverURL, + storeLogoURL: store.logoURL, + storeCategory: store.category, + storeRating: store.rating, + storeDistance: store.distance, + storeDeliveryFee: store.deliveryFee, + appState: $appState + ) + } label: { + FeaturedStoreCard( + store: store, + onFavoriteToggle: { + Task { + await toggleFavoriteStore(storeId: store.id, storeName: store.name) + } + } + ) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 20) + } + private func header(collapseProgress: CGFloat, height: CGFloat) -> some View { let titleOpacity = 1 - clamp(value: collapseProgress * 1.2, lower: 0, upper: 1) let topRowOpacity = 1 - clamp(value: collapseProgress * 1.4, lower: 0, upper: 1) @@ -277,7 +316,7 @@ struct HomeView: View { .opacity(titleOpacity) .offset(y: collapseProgress * -20) } else { - Text("O que vai querer pedir hoje?\n ") + Text("O que vai querer \npedir hoje?") .font(AppTypography.heading2) .foregroundStyle(AppColors.textInverse) .opacity(titleOpacity) @@ -359,8 +398,14 @@ struct HomeView: View { refreshCategories: Bool = false ) async { if isLoadingStores { return } + // A refresh (pull-to-refresh) that fails must never wipe the list + // the user is already looking at — only a first load with nothing + // yet loaded is allowed to show a blocking error state. + let hadExistingStores = stores.isEmpty == false isLoadingStores = true - storesError = nil + if hadExistingStores == false { + storesError = nil + } let coordinate = await resolveCoordinates(forceRefresh: forceLocationRefresh) let hasAddress = hasConfiguredAddress() @@ -415,8 +460,10 @@ struct HomeView: View { ) isLoadingStores = false if response.error { - stores = [] - storesError = response.message ?? "Não foi possível carregar os estabelecimentos." + reportStoresLoadFailure( + response.message ?? "Não foi possível carregar os estabelecimentos.", + hadExistingStores: hadExistingStores + ) return } let results = response.result ?? [] @@ -447,8 +494,16 @@ struct HomeView: View { return } isLoadingStores = false + reportStoresLoadFailure(storesUserMessage(error), hadExistingStores: hadExistingStores) + } + } + + private func reportStoresLoadFailure(_ message: String, hadExistingStores: Bool) { + if hadExistingStores { + SnackbarCenter.shared.show(title: message, style: .warning, icon: "exclamationmark.triangle.fill", duration: 3.0) + } else { stores = [] - storesError = storesUserMessage(error) + storesError = message } } diff --git a/pedi-foods/Sources/PediFoods/Views/Main/HomeViewComponents.swift b/Sources/PediFoods/Views/Main/HomeViewComponents.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/HomeViewComponents.swift rename to Sources/PediFoods/Views/Main/HomeViewComponents.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/MainTabView.swift b/Sources/PediFoods/Views/Main/MainTabView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/MainTabView.swift rename to Sources/PediFoods/Views/Main/MainTabView.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/OrderDetailsView.swift b/Sources/PediFoods/Views/Main/OrderDetailsView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/OrderDetailsView.swift rename to Sources/PediFoods/Views/Main/OrderDetailsView.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/OrderTrackingView.swift b/Sources/PediFoods/Views/Main/OrderTrackingView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/OrderTrackingView.swift rename to Sources/PediFoods/Views/Main/OrderTrackingView.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/OrdersView.swift b/Sources/PediFoods/Views/Main/OrdersView.swift similarity index 99% rename from pedi-foods/Sources/PediFoods/Views/Main/OrdersView.swift rename to Sources/PediFoods/Views/Main/OrdersView.swift index 32bd5c6..4c0b62b 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/OrdersView.swift +++ b/Sources/PediFoods/Views/Main/OrdersView.swift @@ -52,8 +52,12 @@ struct OrdersView: View { await refreshStoreRatings() } .refreshable { - await loadOrders(force: true) - await refreshStoreRatings() + // Decoupled from .refreshable's own cancellable wrapping Task — + // see StoreDetailView's .refreshable for why. + await Task { + await loadOrders(force: true) + await refreshStoreRatings() + }.value } .navigationDestination(item: $selectedOrderRoute) { context in OrderEntryDestinationView( diff --git a/pedi-foods/Sources/PediFoods/Views/Main/PizzaFlavorAddonsSheet.swift b/Sources/PediFoods/Views/Main/PizzaFlavorAddonsSheet.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/PizzaFlavorAddonsSheet.swift rename to Sources/PediFoods/Views/Main/PizzaFlavorAddonsSheet.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/PizzaProductDetailSheet+Flow.swift b/Sources/PediFoods/Views/Main/PizzaProductDetailSheet+Flow.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/PizzaProductDetailSheet+Flow.swift rename to Sources/PediFoods/Views/Main/PizzaProductDetailSheet+Flow.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/PizzaProductDetailSheet.swift b/Sources/PediFoods/Views/Main/PizzaProductDetailSheet.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/PizzaProductDetailSheet.swift rename to Sources/PediFoods/Views/Main/PizzaProductDetailSheet.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/ProductDetailSheet.swift b/Sources/PediFoods/Views/Main/ProductDetailSheet.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/ProductDetailSheet.swift rename to Sources/PediFoods/Views/Main/ProductDetailSheet.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/ProfileView.swift b/Sources/PediFoods/Views/Main/ProfileView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/ProfileView.swift rename to Sources/PediFoods/Views/Main/ProfileView.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/ReviewsView.swift b/Sources/PediFoods/Views/Main/ReviewsView.swift similarity index 99% rename from pedi-foods/Sources/PediFoods/Views/Main/ReviewsView.swift rename to Sources/PediFoods/Views/Main/ReviewsView.swift index f14094b..cacaaff 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/ReviewsView.swift +++ b/Sources/PediFoods/Views/Main/ReviewsView.swift @@ -80,7 +80,9 @@ struct MyReviewsView: View { Task { await loadReviewsFromBackend(forceRefresh: true) } } .refreshable { - await loadReviewsFromBackend(forceRefresh: true) + // Decoupled from .refreshable's own cancellable wrapping Task — + // see StoreDetailView's .refreshable for why. + await Task { await loadReviewsFromBackend(forceRefresh: true) }.value } .task { guard didHandleInitialRoute == false else { return } diff --git a/pedi-foods/Sources/PediFoods/Views/Main/SavedCardsView.swift b/Sources/PediFoods/Views/Main/SavedCardsView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/SavedCardsView.swift rename to Sources/PediFoods/Views/Main/SavedCardsView.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailPizzaSupport.swift b/Sources/PediFoods/Views/Main/StoreDetailPizzaSupport.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/StoreDetailPizzaSupport.swift rename to Sources/PediFoods/Views/Main/StoreDetailPizzaSupport.swift diff --git a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailSupport.swift b/Sources/PediFoods/Views/Main/StoreDetailSupport.swift similarity index 75% rename from pedi-foods/Sources/PediFoods/Views/Main/StoreDetailSupport.swift rename to Sources/PediFoods/Views/Main/StoreDetailSupport.swift index c303c68..a1e2e74 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailSupport.swift +++ b/Sources/PediFoods/Views/Main/StoreDetailSupport.swift @@ -48,14 +48,21 @@ struct ScrollOffsetReader: View { struct AsyncStoreImage: View { let imageURL: String? var fallbackImageName: String = "placeholder-product" + var fitMode: ImageFitMode = .fill var body: some View { - CachedRemoteImage(imageURL: imageURL) { - fallback + GeometryReader { geometry in + CachedRemoteImage(imageURL: imageURL, fitMode: fitMode) { + fallback + } + // Lock the scaledToFill image to the actually proposed box. + // Without this, a wide/landscape source image's fill-scaled + // ideal width can exceed the box and balloon the parent + // ZStack's ideal width, pushing sibling content off-screen. + .frame(width: geometry.size.width, height: geometry.size.height) + .clipped() } - .frame(maxWidth: .infinity, maxHeight: .infinity) .background(AppColors.brandSoft) - .clipped() } private var fallback: some View { diff --git a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView+Components.swift b/Sources/PediFoods/Views/Main/StoreDetailView+Components.swift similarity index 96% rename from pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView+Components.swift rename to Sources/PediFoods/Views/Main/StoreDetailView+Components.swift index 5924dbc..8186947 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView+Components.swift +++ b/Sources/PediFoods/Views/Main/StoreDetailView+Components.swift @@ -25,7 +25,7 @@ extension StoreDetailView { var heroSection: some View { ZStack(alignment: .top) { - AsyncStoreImage(imageURL: resolvedURL(storeCoverURL)) + AsyncStoreImage(imageURL: resolvedURL(storeCoverURL), fitMode: .heightFit) .frame(height: topSectionHeight + stretchAmount) .offset(y: -stretchAmount) .ignoresSafeArea(.container, edges: .top) @@ -139,7 +139,12 @@ extension StoreDetailView { @ViewBuilder var sectionedProducts: some View { - if isLoading { + // Once we have categories loaded, keep showing them regardless of a + // subsequent refresh's isLoading/errorMessage state — a failed or + // in-flight pull-to-refresh must never hide already-loaded content. + if categories.isEmpty == false { + sectionedProductsList + } else if isLoading { VStack(spacing: 10) { ProgressView() Text("Carregando cardápio...") @@ -159,14 +164,18 @@ extension StoreDetailView { .foregroundStyle(AppColors.primary) } .padding(.top, 16) - } else if categories.isEmpty { + } else { Text("Cardápio indisponível no momento.") .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) .padding(.top, 16) .padding(.horizontal, 16) .padding(.bottom, 120) - } else { + } + } + + @ViewBuilder + private var sectionedProductsList: some View { ForEach(categories, id: \.id) { category in Text(category.name) .font(AppTypography.heading2) @@ -195,7 +204,6 @@ extension StoreDetailView { .padding(.bottom, 8) } Color.clear.frame(height: 140) - } } func categoryTabs(proxy: ScrollViewProxy, isPinned: Bool = false) -> some View { @@ -344,7 +352,7 @@ extension StoreDetailView { .appContentShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) .onTapGesture { guard isStoreOpen else { - SnackbarCenter.shared.show(title: "Loja fechada no momento.", style: .warning, icon: "moon.zzz.fill", duration: 2.0) + SnackbarCenter.shared.show(title: "Loja fechada no momento.", style: .warning, icon: "moon.zzz.fill", duration: 2.0) return } if item.isPizzaSummary, let pizzaCategoryId = item.pizzaCategoryId { diff --git a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView+Logic.swift b/Sources/PediFoods/Views/Main/StoreDetailView+Logic.swift similarity index 85% rename from pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView+Logic.swift rename to Sources/PediFoods/Views/Main/StoreDetailView+Logic.swift index 3de4194..cd0f82b 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView+Logic.swift +++ b/Sources/PediFoods/Views/Main/StoreDetailView+Logic.swift @@ -51,8 +51,8 @@ extension StoreDetailView { } var deliveryValueLabel: String { - if let fee = storeDeliveryFee { - return formatCurrency(fee) + if let minOrder = info?.minOrder { + return formatCurrency(minOrder) } return "R$ --" } @@ -135,8 +135,14 @@ extension StoreDetailView { @MainActor func loadStoreData(forceRefresh: Bool = false) async { + // A refresh (pull-to-refresh) that fails must never wipe content the + // user is already looking at — only a first load with nothing yet + // loaded is allowed to show a blocking error state. + let hadExistingContent = categories.isEmpty == false isLoading = true - errorMessage = nil + if hadExistingContent == false { + errorMessage = nil + } let infoCacheKey = "store-info:\(storeId)" let catalogCacheKey = "store-catalog:\(storeId)" @@ -166,13 +172,13 @@ extension StoreDetailView { let catalogResponse = try await apiService.storeCatalog(storeId: storeId) if infoResponse.error { - errorMessage = infoResponse.message ?? "Não foi possível carregar a loja." isLoading = false + reportStoreLoadFailure(infoResponse.message ?? "Não foi possível carregar a loja.", hadExistingContent: hadExistingContent) return } if catalogResponse.error { - errorMessage = catalogResponse.message ?? "Não foi possível carregar o catálogo." isLoading = false + reportStoreLoadFailure(catalogResponse.message ?? "Não foi possível carregar o catálogo.", hadExistingContent: hadExistingContent) return } @@ -191,18 +197,55 @@ extension StoreDetailView { AppContentCache.shared.set(info, for: infoCacheKey, ttl: AppCacheTTL.twoHours) } AppContentCache.shared.set(normalizedCatalog, for: catalogCacheKey, ttl: AppCacheTTL.twoHours) + errorMessage = nil isLoading = false } catch { isLoading = false + // A cancelled request (e.g. the .refreshable task torn down by a + // re-render, or superseded by a newer pull) is not a failure — + // it never got a response either way, so there is nothing to + // report and no content to touch. + if isCancelledRequest(error) { + return + } + let message: String if let network = error as? NetworkError { - errorMessage = network.errorDescription ?? "Erro ao carregar loja." - return + message = network.errorDescription ?? "Erro ao carregar loja." + } else if let service = error as? ApiServiceError { + message = service.errorDescription ?? "Erro ao carregar loja." + } else { + message = "Erro ao carregar loja." } - if let service = error as? ApiServiceError { - errorMessage = service.errorDescription ?? "Erro ao carregar loja." - return + reportStoreLoadFailure(message, hadExistingContent: hadExistingContent) + } + } + + private func isCancelledRequest(_ error: Error) -> Bool { + if error is CancellationError { + return true + } + + if let networkError = error as? NetworkError { + switch networkError { + case .cancelled: + return true + case .transportError(let message): + let normalized = message.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + return normalized.contains("cancel") + default: + break } - errorMessage = "Erro ao carregar loja." + } + + return error.localizedDescription.lowercased().contains("cancel") + } + + @MainActor + private func reportStoreLoadFailure(_ message: String, hadExistingContent: Bool) { + if hadExistingContent { + SnackbarCenter.shared.show(title: message, style: .warning, icon: "exclamationmark.triangle.fill", duration: 3.0) + } else { + errorMessage = message } } diff --git a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView.swift b/Sources/PediFoods/Views/Main/StoreDetailView.swift similarity index 76% rename from pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView.swift rename to Sources/PediFoods/Views/Main/StoreDetailView.swift index 5e6761a..91fe354 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView.swift +++ b/Sources/PediFoods/Views/Main/StoreDetailView.swift @@ -61,6 +61,20 @@ struct StoreDetailView: View { ScrollOffsetReader(offsetY: $scrollOffset) LazyVStack(spacing: 0) { topSection + // Guaranteed-visible refresh feedback, right below + // the hero — not relying on the native spinner's + // position (unreliable here, see .refreshable note + // below). + if isLoading && categories.isEmpty == false { + HStack(spacing: 8) { + ProgressView() + Text("Atualizando...") + .font(AppTypography.caption) + .foregroundStyle(AppColors.textMuted) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 10) + } categoryTabs(proxy: proxy, isPinned: false) .opacity(isCategoryTabsPinned ? 0 : 1) sectionedProducts @@ -68,9 +82,24 @@ struct StoreDetailView: View { } .coordinateSpace(name: StoreDetailScrollCoordinateSpace.name) .refreshable { - await loadStoreData(forceRefresh: true) + // Run the actual load in its own unstructured Task and + // await that, instead of awaiting loadStoreData directly + // in this closure. SwiftUI can cancel .refreshable's own + // wrapping Task (e.g. the gesture not fully "committing") + // independent of whether the network call is still + // legitimately in flight. Awaiting Task.value here + // blocks until the detached load genuinely finishes + // (success, error, or our own 20s ApiClient timeout), + // so a premature refreshable-cancellation can no longer + // silently swallow a real in-flight request. + await Task { await loadStoreData(forceRefresh: true) }.value } - .ignoresSafeArea(edges: .top) + // NOT .ignoresSafeArea here: combined with .refreshable on + // the same view, it breaks the native pull-to-refresh + // spinner's positioning (renders invisible/off-place) even + // though the gesture still fires the closure. The hero + // image and gradient above already bleed under the status + // bar independently via their own .ignoresSafeArea calls. .background(AppColors.backgroundLight) categoryTabs(proxy: proxy, isPinned: true) diff --git a/pedi-foods/Sources/PediFoods/Views/Main/UserProfileView.swift b/Sources/PediFoods/Views/Main/UserProfileView.swift similarity index 100% rename from pedi-foods/Sources/PediFoods/Views/Main/UserProfileView.swift rename to Sources/PediFoods/Views/Main/UserProfileView.swift diff --git a/pedi-foods/Tests/PediFoodsTests/FavoritesDecodingTests.swift b/Tests/PediFoodsTests/FavoritesDecodingTests.swift similarity index 100% rename from pedi-foods/Tests/PediFoodsTests/FavoritesDecodingTests.swift rename to Tests/PediFoodsTests/FavoritesDecodingTests.swift diff --git a/pedi-foods/Tests/PediFoodsTests/SmokeTests.swift b/Tests/PediFoodsTests/SmokeTests.swift similarity index 100% rename from pedi-foods/Tests/PediFoodsTests/SmokeTests.swift rename to Tests/PediFoodsTests/SmokeTests.swift diff --git a/pedi-foods/Tests/PediFoodsTests/StoreCatalogNormalizerTests.swift b/Tests/PediFoodsTests/StoreCatalogNormalizerTests.swift similarity index 100% rename from pedi-foods/Tests/PediFoodsTests/StoreCatalogNormalizerTests.swift rename to Tests/PediFoodsTests/StoreCatalogNormalizerTests.swift diff --git a/feature-control-bff/README.md b/feature-control-bff/README.md deleted file mode 100644 index 7b57000..0000000 --- a/feature-control-bff/README.md +++ /dev/null @@ -1,108 +0,0 @@ -# Feature Control BFF - -BFF mínimo para expor flags ao app sem expor `Atomenta-Token` no cliente. - -## Endpoint - -- `POST /feature-control/bootstrap` -- `POST /feature-control/telemetry/exposure` -- `GET /health` - -## Notificações - -No momento este BFF cobre apenas Feature Control (`bootstrap` + `exposure`). -Para notificações segmentadas por audiência (`todos`, `feature:fc.ios`), precisamos adicionar o contrato de endpoint de notificações do Atomenta para integrar no app. - -## Variáveis de ambiente - -- `PORT` (default: `8787`) -- `ATOMENTA_ORIGIN` (default: `https://atomenta.com.br`) -- `FEATURE_CONTROL_MODULE_TOKEN` (ou `ATOMENTA_FEATURE_CONTROL_MODULE_ID`) **obrigatório** -- `FEATURE_CONTROL_ENVIRONMENT` (default: `production`) -- `FEATURE_CONTROL_CACHE_TTL_MS` (default: `60000`) -- `FEATURE_CONTROL_TIMEOUT_MS` (default: `2000`) -- `FEATURE_CONTROL_DEFAULTS_JSON` (opcional, JSON com defaults de fallback) - -Exemplo de `FEATURE_CONTROL_DEFAULTS_JSON`: - -```json -{ - "at.ios.only": { "enabled": true, "variant": "on" }, - "at.android.only": { "enabled": true, "variant": "on" }, - "at.promo": { "enabled": true, "variant": "on" }, - "at.city.aguai": { "enabled": true, "variant": "on" } -} -``` - -## Request (App -> BFF) - -```json -{ - "environment": "production", - "keys": ["at.ios.only", "at.android.only", "at.promo", "at.city.aguai"], - "context": { - "subjectType": "customer", - "subjectId": "cust_123", - "storeId": "store_001", - "platform": "ios", - "appVersion": "2.3.1", - "attributes": { - "city": "Aguaí" - } - } -} -``` - -Se o app enviar `Authorization: Bearer `, o BFF tenta extrair `subjectId` do JWT (`sub`/`customerId`/`id`) quando `context.subjectId` não vier. - -## Response (BFF -> App) - -```json -{ - "ok": true, - "source": "live", - "configVersion": 7, - "evaluatedAt": "2026-04-16T12:00:00.000Z", - "flags": { - "atiosonly": true, - "atandroidonly": false, - "atpromo": true, - "atcityaguai": true - }, - "raw": { - "at.ios.only": { - "enabled": true, - "variant": "on", - "payload": null, - "reason": "rollout" - }, - "at.promo": { - "enabled": true, - "variant": "on", - "payload": null, - "reason": "rollout" - } - } -} -``` - -> O app Swift usa o campo **`raw`** indexado pela chave original (ex: `raw["at.promo"]`). O campo `flags` é uma versão simplificada gerada mecanicamente (pontos removidos). -``` - -Quando o upstream falha (ex.: `429`, `500`, timeout), o BFF devolve `fallback` com defaults estáticos. - -## Execução local - -```bash -cd feature-control-bff -FEATURE_CONTROL_MODULE_TOKEN="550e8400-e29b-41d4-a716-44665544000b" npm start -``` - -## Testes - -```bash -cd feature-control-bff -npm test -``` - -Autor: Daniel Arantes Loverde diff --git a/feature-control-bff/core.mjs b/feature-control-bff/core.mjs deleted file mode 100644 index d5e3a22..0000000 --- a/feature-control-bff/core.mjs +++ /dev/null @@ -1,333 +0,0 @@ -import crypto from "node:crypto"; - -export function loadConfig(env = process.env) { - return { - atomentaOrigin: (env.ATOMENTA_ORIGIN || "https://atomenta.com.br").replace(/\/+$/, ""), - moduleToken: env.FEATURE_CONTROL_MODULE_TOKEN || env.ATOMENTA_FEATURE_CONTROL_MODULE_ID || "", - defaultEnvironment: env.FEATURE_CONTROL_ENVIRONMENT || "production", - cacheTtlMs: Number(env.FEATURE_CONTROL_CACHE_TTL_MS || 60_000), - requestTimeoutMs: Number(env.FEATURE_CONTROL_TIMEOUT_MS || 2_000), - staticDefaults: parseDefaults(env.FEATURE_CONTROL_DEFAULTS_JSON) - }; -} - -export function createRuntime(config) { - return { cache: new Map(), inFlight: new Map(), config }; -} - -export function nowIso() { - return new Date().toISOString(); -} - -export function writeJson(res, status, payload) { - const body = JSON.stringify(payload); - res.writeHead(status, { - "Content-Type": "application/json; charset=utf-8", - "Content-Length": Buffer.byteLength(body), - "Cache-Control": "no-store" - }); - res.end(body); -} - -const MAX_BODY_BYTES = 128 * 1024; -const MAX_KEYS = 100; -const MAX_ATTRIBUTES_BYTES = 16 * 1024; -const MAX_EXPOSURE_EVENTS = 100; - -function parseDefaults(raw) { - if (!raw) return {}; - const parsed = safeParseJson(raw); - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {}; - return parsed; -} - -function safeParseJson(input) { - try { - return JSON.parse(input); - } catch { - return null; - } -} - -function normalizeString(value) { - return typeof value === "string" ? value.trim() : ""; -} - -function getSubjectFromAuthHeader(authHeader) { - if (!authHeader || !authHeader.toLowerCase().startsWith("bearer ")) return null; - const token = authHeader.slice(7).trim(); - const parts = token.split("."); - if (parts.length < 2) return null; - - try { - const payloadBase64 = parts[1].replace(/-/g, "+").replace(/_/g, "/"); - const padded = payloadBase64 + "=".repeat((4 - (payloadBase64.length % 4)) % 4); - const payload = JSON.parse(Buffer.from(padded, "base64").toString("utf-8")); - const subjectId = normalizeString(payload.sub) || normalizeString(payload.customerId) || normalizeString(payload.id); - if (!subjectId) return null; - return { subjectType: "customer", subjectId }; - } catch { - return null; - } -} - -function buildCacheKey(input) { - const stable = JSON.stringify({ - env: input.environment, - keys: input.keys, - context: { - subjectType: input.context.subjectType, - subjectId: input.context.subjectId, - storeId: input.context.storeId || "", - platform: input.context.platform || "", - appVersion: input.context.appVersion || "", - attributes: input.context.attributes || {} - } - }); - return crypto.createHash("sha256").update(stable).digest("hex"); -} - -async function readBody(req) { - const chunks = []; - let size = 0; - for await (const chunk of req) { - size += chunk.length; - if (size > MAX_BODY_BYTES) { - throw Object.assign(new Error("Payload muito grande"), { status: 413, code: "PAYLOAD_TOO_LARGE" }); - } - chunks.push(chunk); - } - return Buffer.concat(chunks).toString("utf-8"); -} - -function validateBootstrapRequest(body, authHeader, config) { - if (!body || typeof body !== "object" || Array.isArray(body)) { - throw Object.assign(new Error("Body inválido"), { status: 400, code: "INVALID_BODY" }); - } - - const keys = Array.isArray(body.keys) ? body.keys.map(normalizeString).filter(Boolean) : []; - if (!keys.length) throw Object.assign(new Error("`keys` é obrigatório"), { status: 400, code: "FEATURE_CONTROL_KEYS_REQUIRED" }); - if (keys.length > MAX_KEYS) { - throw Object.assign(new Error(`Máximo de ${MAX_KEYS} chaves por request`), { status: 400, code: "FEATURE_CONTROL_TOO_MANY_KEYS" }); - } - - const environment = normalizeString(body.environment) || config.defaultEnvironment; - const contextInput = body.context && typeof body.context === "object" ? body.context : {}; - const fromJwt = getSubjectFromAuthHeader(authHeader); - const subjectType = normalizeString(contextInput.subjectType) || fromJwt?.subjectType || "customer"; - const subjectId = normalizeString(contextInput.subjectId) || fromJwt?.subjectId || ""; - if (!subjectId) { - throw Object.assign(new Error("`context.subjectId` é obrigatório"), { status: 400, code: "FEATURE_CONTROL_SUBJECT_ID_REQUIRED" }); - } - - const allowed = new Set(["user", "customer", "store", "anonymous"]); - if (!allowed.has(subjectType)) { - throw Object.assign(new Error("`context.subjectType` inválido"), { status: 400, code: "FEATURE_CONTROL_SUBJECT_TYPE_INVALID" }); - } - - const attributes = contextInput.attributes && typeof contextInput.attributes === "object" && !Array.isArray(contextInput.attributes) - ? contextInput.attributes - : {}; - - if (Buffer.byteLength(JSON.stringify(attributes), "utf-8") > MAX_ATTRIBUTES_BYTES) { - throw Object.assign(new Error("`context.attributes` excede limite"), { status: 400, code: "FEATURE_CONTROL_ATTRIBUTES_TOO_LARGE" }); - } - - return { - environment, - keys: [...new Set(keys)].sort(), - context: { - subjectType, - subjectId, - storeId: normalizeString(contextInput.storeId) || undefined, - platform: normalizeString(contextInput.platform) || undefined, - appVersion: normalizeString(contextInput.appVersion) || undefined, - attributes - } - }; -} - -function validateExposureRequest(body) { - if (!body || typeof body !== "object" || Array.isArray(body)) { - throw Object.assign(new Error("Body inválido"), { status: 400, code: "INVALID_BODY" }); - } - if (!Array.isArray(body.events) || body.events.length === 0) { - throw Object.assign(new Error("`events` é obrigatório"), { status: 400, code: "FEATURE_CONTROL_EVENTS_REQUIRED" }); - } - if (body.events.length > MAX_EXPOSURE_EVENTS) { - throw Object.assign(new Error(`Máximo de ${MAX_EXPOSURE_EVENTS} eventos por request`), { - status: 400, - code: "FEATURE_CONTROL_EXPOSURE_TOO_MANY_EVENTS" - }); - } - - const events = body.events.map((event) => ({ - featureKey: normalizeString(event?.featureKey), - variant: normalizeString(event?.variant) || "off", - subjectType: normalizeString(event?.subjectType) || "customer", - storeId: normalizeString(event?.storeId) || undefined - })); - - for (const event of events) { - if (!event.featureKey) { - throw Object.assign(new Error("`featureKey` é obrigatório"), { status: 400, code: "FEATURE_CONTROL_EXPOSURE_FEATURE_KEY_REQUIRED" }); - } - } - return { events }; -} - -function buildFallbackFlags(keys, config) { - const raw = {}; - const simplified = {}; - - for (const key of keys) { - const fromEnv = config.staticDefaults[key]; - const enabled = Boolean(fromEnv?.enabled); - const variant = typeof fromEnv?.variant === "string" ? fromEnv.variant : enabled ? "on" : "off"; - raw[key] = { enabled, variant, payload: fromEnv?.payload ?? null, reason: "fallback_default" }; - } - - for (const [key, value] of Object.entries(raw)) { - const simplifiedKey = key.replace(/^fc\./, "").replace(/[_-]([a-z])/g, (_, c) => c.toUpperCase()).replace(/\./g, ""); - if (!simplified[simplifiedKey]) { - simplified[simplifiedKey] = value.variant === "on" || value.enabled === true ? true : value.variant; - } - } - - return { raw, simplified }; -} - -function mapUpstreamError(error) { - return { - status: Number(error?.status || 500), - code: normalizeString(error?.code) || "FEATURE_CONTROL_BFF_ERROR", - message: normalizeString(error?.message) || "Erro ao avaliar flags" - }; -} - -async function callAtomenta(path, requestPayload, config) { - if (!config.moduleToken) { - throw Object.assign(new Error("Token de módulo não configurado"), { status: 500, code: "FEATURE_CONTROL_MODULE_TOKEN_MISSING" }); - } - - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), config.requestTimeoutMs); - - try { - const response = await fetch(`${config.atomentaOrigin}${path}`, { - method: "POST", - signal: controller.signal, - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "Atomenta-Token": config.moduleToken - }, - body: JSON.stringify(requestPayload) - }); - - const text = await response.text(); - const parsed = safeParseJson(text) || {}; - if (!response.ok) { - const upstreamCode = parsed.code || "FEATURE_CONTROL_UPSTREAM_ERROR"; - const upstreamMessage = parsed.message || `Erro upstream (${response.status})`; - throw Object.assign(new Error(upstreamMessage), { status: response.status, code: upstreamCode }); - } - return parsed; - } catch (error) { - if (error.name === "AbortError") { - throw Object.assign(new Error("Timeout no upstream"), { status: 504, code: "FEATURE_CONTROL_UPSTREAM_TIMEOUT" }); - } - throw error; - } finally { - clearTimeout(timeout); - } -} - -function formatBffResponse(evaluated, requestPayload, source = "live") { - const result = evaluated?.result || {}; - const flags = result.flags && typeof result.flags === "object" ? result.flags : {}; - const simplified = {}; - for (const key of requestPayload.keys) { - const value = flags[key]; - if (!value || typeof value !== "object") continue; - const simplifiedKey = key.replace(/^fc\./, "").replace(/[_-]([a-z])/g, (_, c) => c.toUpperCase()).replace(/\./g, ""); - if (!simplified[simplifiedKey]) simplified[simplifiedKey] = value.variant === "on" || value.enabled === true ? true : value.variant || "off"; - } - return { - ok: true, - source, - configVersion: Number(result.configVersion || 0), - evaluatedAt: result.evaluatedAt || nowIso(), - flags: simplified, - raw: flags - }; -} - -async function evaluateWithCache(requestPayload, runtime) { - const cacheKey = buildCacheKey(requestPayload); - const current = runtime.cache.get(cacheKey); - if (current && current.expiresAt > Date.now()) return current.payload; - - const inflight = runtime.inFlight.get(cacheKey); - if (inflight) return inflight; - - const work = (async () => { - const evaluated = await callAtomenta("/api/feature-control/evaluate", requestPayload, runtime.config); - const payload = formatBffResponse(evaluated, requestPayload, "live"); - runtime.cache.set(cacheKey, { payload, expiresAt: Date.now() + runtime.config.cacheTtlMs }); - return payload; - })(); - runtime.inFlight.set(cacheKey, work); - - try { - return await work; - } finally { - runtime.inFlight.delete(cacheKey); - } -} - -export async function handleBootstrap(req, res, runtime) { - try { - const body = safeParseJson(await readBody(req)); - if (!body) return writeJson(res, 400, { ok: false, code: "INVALID_JSON", message: "JSON inválido" }); - const requestPayload = validateBootstrapRequest(body, req.headers.authorization, runtime.config); - try { - const payload = await evaluateWithCache(requestPayload, runtime); - return writeJson(res, 200, payload); - } catch (error) { - const mapped = mapUpstreamError(error); - const fallback = buildFallbackFlags(requestPayload.keys, runtime.config); - return writeJson(res, mapped.status === 400 ? 400 : 200, { - ok: mapped.status === 400 ? false : true, - source: "fallback", - configVersion: 0, - evaluatedAt: nowIso(), - flags: fallback.simplified, - raw: fallback.raw, - upstreamError: mapped - }); - } - } catch (error) { - return writeJson(res, Number(error?.status || 500), { - ok: false, - code: normalizeString(error?.code) || "FEATURE_CONTROL_BFF_ERROR", - message: normalizeString(error?.message) || "Erro interno" - }); - } -} - -export async function handleExposure(req, res, runtime) { - try { - const body = safeParseJson(await readBody(req)); - if (!body) return writeJson(res, 400, { ok: false, code: "INVALID_JSON", message: "JSON inválido" }); - const requestPayload = validateExposureRequest(body); - const response = await callAtomenta("/api/feature-control/telemetry/exposure", requestPayload, runtime.config); - return writeJson(res, 200, { ok: true, code: "FEATURE_CONTROL_EXPOSURE_ACCEPTED", result: response?.result ?? null }); - } catch (error) { - return writeJson(res, Number(error?.status || 500), { - ok: false, - code: normalizeString(error?.code) || "FEATURE_CONTROL_EXPOSURE_ERROR", - message: normalizeString(error?.message) || "Falha ao enviar exposição" - }); - } -} diff --git a/feature-control-bff/package.json b/feature-control-bff/package.json deleted file mode 100644 index 6e8d9f9..0000000 --- a/feature-control-bff/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "feature-control-bff", - "version": "1.0.0", - "private": true, - "type": "module", - "scripts": { - "start": "node server.mjs", - "test": "node --test tests/*.test.mjs" - } -} diff --git a/feature-control-bff/server.mjs b/feature-control-bff/server.mjs deleted file mode 100644 index e8188cc..0000000 --- a/feature-control-bff/server.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import http from "node:http"; -import { createRuntime, handleBootstrap, handleExposure, loadConfig, nowIso, writeJson } from "./core.mjs"; - -const PORT = Number(process.env.PORT || 8787); -const runtime = createRuntime(loadConfig(process.env)); - -const server = http.createServer(async (req, res) => { - const method = req.method || "GET"; - const url = req.url || "/"; - - if (method === "GET" && url === "/health") { - return writeJson(res, 200, { - ok: true, - service: "feature-control-bff", - time: nowIso(), - cacheEntries: runtime.cache.size, - inFlight: runtime.inFlight.size - }); - } - - if (method === "POST" && url === "/feature-control/bootstrap") { - return handleBootstrap(req, res, runtime); - } - - if (method === "POST" && url === "/feature-control/telemetry/exposure") { - return handleExposure(req, res, runtime); - } - - return writeJson(res, 404, { ok: false, code: "NOT_FOUND", message: "Rota não encontrada" }); -}); - -server.listen(PORT, () => { - console.log(`[feature-control-bff] listening on :${PORT}`); -}); diff --git a/feature-control-bff/tests/bff.test.mjs b/feature-control-bff/tests/bff.test.mjs deleted file mode 100644 index fa41cc3..0000000 --- a/feature-control-bff/tests/bff.test.mjs +++ /dev/null @@ -1,277 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import http from "node:http"; -import { spawn } from "node:child_process"; - -function listen(server) { - return new Promise((resolve) => { - server.listen(0, "127.0.0.1", () => { - const address = server.address(); - resolve(address.port); - }); - }); -} - -function closeServer(server) { - return new Promise((resolve) => server.close(() => resolve())); -} - -async function waitForHealth(url, timeoutMs = 5000) { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - try { - const response = await fetch(`${url}/health`); - if (response.ok) return; - } catch {} - await new Promise((resolve) => setTimeout(resolve, 100)); - } - throw new Error("BFF health timeout"); -} - -async function startBff(env) { - const child = spawn("node", ["server.mjs"], { - cwd: new URL("..", import.meta.url), - env: { ...process.env, ...env }, - stdio: ["ignore", "pipe", "pipe"] - }); - - const logs = []; - child.stdout.on("data", (chunk) => logs.push(chunk.toString("utf-8"))); - child.stderr.on("data", (chunk) => logs.push(chunk.toString("utf-8"))); - - const port = Number(env.PORT); - const baseUrl = `http://127.0.0.1:${port}`; - await waitForHealth(baseUrl); - - return { - baseUrl, - logs, - stop: async () => { - child.kill("SIGTERM"); - await new Promise((resolve) => child.on("exit", () => resolve())); - } - }; -} - -function createUpstreamStub(options = {}) { - const state = { - evaluateMode: options.evaluateMode || "success", - evaluatePayload: null, - exposurePayload: null - }; - - const server = http.createServer(async (req, res) => { - if (req.method === "POST" && req.url === "/api/feature-control/evaluate") { - const chunks = []; - for await (const chunk of req) chunks.push(chunk); - state.evaluatePayload = JSON.parse(Buffer.concat(chunks).toString("utf-8")); - - if (state.evaluateMode === "429") { - res.writeHead(429, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ code: "FEATURE_CONTROL_RATE_LIMIT", message: "rate" })); - return; - } - - if (state.evaluateMode === "500") { - res.writeHead(500, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ code: "FEATURE_CONTROL_ERROR", message: "boom" })); - return; - } - - if (state.evaluateMode === "timeout") { - await new Promise((resolve) => setTimeout(resolve, 600)); - } - - res.writeHead(200, { "Content-Type": "application/json" }); - res.end( - JSON.stringify({ - error: false, - code: "FEATURE_CONTROL_EVALUATED", - result: { - evaluatedAt: "2026-04-16T12:00:00.000Z", - configVersion: 7, - flags: { - "fc.promo": { enabled: true, variant: "on", payload: null, reason: "rollout" }, - "fc.promo-codes": { enabled: true, variant: "on", payload: null, reason: "rollout" }, - "fc.ios": { enabled: true, variant: "on", payload: null, reason: "audience" } - } - } - }) - ); - return; - } - - if (req.method === "POST" && req.url === "/api/feature-control/telemetry/exposure") { - const chunks = []; - for await (const chunk of req) chunks.push(chunk); - state.exposurePayload = JSON.parse(Buffer.concat(chunks).toString("utf-8")); - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: false, code: "FEATURE_CONTROL_EXPOSURE_TRACKED", result: { accepted: true } })); - return; - } - - res.writeHead(404).end(); - }); - - return { server, state }; -} - -function bootstrapBody() { - return { - environment: "production", - keys: ["fc.promo", "fc.promo-codes", "fc.ios"], - context: { - subjectType: "customer", - subjectId: "cust_123", - storeId: "store_001", - platform: "ios", - appVersion: "2.3.1", - attributes: { city: "Campinas" } - } - }; -} - -test("bootstrap sucesso retorna flags simplificadas e raw", async () => { - const upstream = createUpstreamStub({ evaluateMode: "success" }); - const upstreamPort = await listen(upstream.server); - const bffPort = upstreamPort + 1000; - const bff = await startBff({ - PORT: String(bffPort), - ATOMENTA_ORIGIN: `http://127.0.0.1:${upstreamPort}`, - FEATURE_CONTROL_MODULE_TOKEN: "token", - FEATURE_CONTROL_TIMEOUT_MS: "300" - }); - - try { - const response = await fetch(`${bff.baseUrl}/feature-control/bootstrap`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(bootstrapBody()) - }); - assert.equal(response.status, 200); - const json = await response.json(); - assert.equal(json.ok, true); - assert.equal(json.configVersion, 7); - assert.equal(json.flags.promo, true); - assert.equal(json.flags.promoCodes, true); - assert.equal(json.flags.ios, true); - assert.ok(json.raw["fc.promo"]); - assert.equal(upstream.state.evaluatePayload.context.subjectId, "cust_123"); - } finally { - await bff.stop(); - await closeServer(upstream.server); - } -}); - -test("bootstrap com 429 devolve fallback sem quebrar cliente", async () => { - const upstream = createUpstreamStub({ evaluateMode: "429" }); - const upstreamPort = await listen(upstream.server); - const bffPort = upstreamPort + 1000; - const bff = await startBff({ - PORT: String(bffPort), - ATOMENTA_ORIGIN: `http://127.0.0.1:${upstreamPort}`, - FEATURE_CONTROL_MODULE_TOKEN: "token", - FEATURE_CONTROL_DEFAULTS_JSON: JSON.stringify({ - "fc.promo": { enabled: false, variant: "off" } - }) - }); - - try { - const response = await fetch(`${bff.baseUrl}/feature-control/bootstrap`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(bootstrapBody()) - }); - assert.equal(response.status, 200); - const json = await response.json(); - assert.equal(json.source, "fallback"); - assert.equal(json.upstreamError.status, 429); - assert.equal(json.flags.promo, "off"); - } finally { - await bff.stop(); - await closeServer(upstream.server); - } -}); - -test("bootstrap com timeout devolve fallback", async () => { - const upstream = createUpstreamStub({ evaluateMode: "timeout" }); - const upstreamPort = await listen(upstream.server); - const bffPort = upstreamPort + 1000; - const bff = await startBff({ - PORT: String(bffPort), - ATOMENTA_ORIGIN: `http://127.0.0.1:${upstreamPort}`, - FEATURE_CONTROL_MODULE_TOKEN: "token", - FEATURE_CONTROL_TIMEOUT_MS: "100" - }); - - try { - const response = await fetch(`${bff.baseUrl}/feature-control/bootstrap`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(bootstrapBody()) - }); - assert.equal(response.status, 200); - const json = await response.json(); - assert.equal(json.source, "fallback"); - assert.equal(json.upstreamError.code, "FEATURE_CONTROL_UPSTREAM_TIMEOUT"); - } finally { - await bff.stop(); - await closeServer(upstream.server); - } -}); - -test("telemetria de exposure encaminha lote para upstream", async () => { - const upstream = createUpstreamStub({ evaluateMode: "success" }); - const upstreamPort = await listen(upstream.server); - const bffPort = upstreamPort + 1000; - const bff = await startBff({ - PORT: String(bffPort), - ATOMENTA_ORIGIN: `http://127.0.0.1:${upstreamPort}`, - FEATURE_CONTROL_MODULE_TOKEN: "token" - }); - - try { - const response = await fetch(`${bff.baseUrl}/feature-control/telemetry/exposure`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - events: [ - { featureKey: "fc.promo", variant: "on", subjectType: "customer", storeId: "store_1" } - ] - }) - }); - assert.equal(response.status, 200); - const json = await response.json(); - assert.equal(json.ok, true); - assert.equal(upstream.state.exposurePayload.events[0].featureKey, "fc.promo"); - } finally { - await bff.stop(); - await closeServer(upstream.server); - } -}); - -test("telemetria rejeita payload sem eventos", async () => { - const upstream = createUpstreamStub({ evaluateMode: "success" }); - const upstreamPort = await listen(upstream.server); - const bffPort = upstreamPort + 1000; - const bff = await startBff({ - PORT: String(bffPort), - ATOMENTA_ORIGIN: `http://127.0.0.1:${upstreamPort}`, - FEATURE_CONTROL_MODULE_TOKEN: "token" - }); - - try { - const response = await fetch(`${bff.baseUrl}/feature-control/telemetry/exposure`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ events: [] }) - }); - assert.equal(response.status, 400); - const json = await response.json(); - assert.equal(json.ok, false); - assert.equal(json.code, "FEATURE_CONTROL_EVENTS_REQUIRED"); - } finally { - await bff.stop(); - await closeServer(upstream.server); - } -}); diff --git a/pedi-foods/Android/app/build.gradle.kts b/pedi-foods/Android/app/build.gradle.kts deleted file mode 100644 index 8a591a5..0000000 --- a/pedi-foods/Android/app/build.gradle.kts +++ /dev/null @@ -1,87 +0,0 @@ -import java.util.Properties - -plugins { - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.compose) - alias(libs.plugins.android.application) - id("skip-build-plugin") -} - -skip { -} - -kotlin { - compilerOptions { - jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.fromTarget(libs.versions.jvm.get().toString()) - } -} - -android { - namespace = group as String - compileSdk = libs.versions.android.sdk.compile.get().toInt() - compileOptions { - sourceCompatibility = JavaVersion.toVersion(libs.versions.jvm.get()) - targetCompatibility = JavaVersion.toVersion(libs.versions.jvm.get()) - } - packaging { - jniLibs { - keepDebugSymbols.add("**/*.so") - pickFirsts.add("**/*.so") - // this option will compress JNI .so files - useLegacyPackaging = true - } - } - - defaultConfig { - minSdk = libs.versions.android.sdk.min.get().toInt() - targetSdk = libs.versions.android.sdk.compile.get().toInt() - // skip.tools.skip-build-plugin will automatically use Skip.env properties for: - // applicationId = ANDROID_APPLICATION_ID ?? PRODUCT_BUNDLE_IDENTIFIER - // versionCode = CURRENT_PROJECT_VERSION - // versionName = MARKETING_VERSION - } - - lint { - disable.add("Instantiatable") - disable.add("MissingPermission") - } - - dependenciesInfo { - // Disables dependency metadata when building APKs. - includeInApk = false - // Disables dependency metadata when building Android App Bundles. - includeInBundle = false - } - - // default signing configuration tries to load from keystore.properties - // see: https://skip.tools/docs/deployment/#export-signing - signingConfigs { - val keystorePropertiesFile = file("keystore.properties") - create("release") { - if (keystorePropertiesFile.isFile) { - val keystoreProperties = Properties() - keystoreProperties.load(keystorePropertiesFile.inputStream()) - keyAlias = keystoreProperties.getProperty("keyAlias") - keyPassword = keystoreProperties.getProperty("keyPassword") - storeFile = file(keystoreProperties.getProperty("storeFile")) - storePassword = keystoreProperties.getProperty("storePassword") - } else { - // when there is no keystore.properties file, fall back to signing with debug config - keyAlias = signingConfigs.getByName("debug").keyAlias - keyPassword = signingConfigs.getByName("debug").keyPassword - storeFile = signingConfigs.getByName("debug").storeFile - storePassword = signingConfigs.getByName("debug").storePassword - } - } - } - - buildTypes { - release { - signingConfig = signingConfigs.findByName("release") - isMinifyEnabled = true - isShrinkResources = true - isDebuggable = false // can be set to true for debugging release build, but needs to be false when uploading to store - proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") - } - } -} diff --git a/pedi-foods/Android/app/proguard-rules.pro b/pedi-foods/Android/app/proguard-rules.pro deleted file mode 100644 index ee51be0..0000000 --- a/pedi-foods/Android/app/proguard-rules.pro +++ /dev/null @@ -1,10 +0,0 @@ --keeppackagenames ** --keep class skip.** { *; } --keep class tools.skip.** { *; } --keep class kotlin.jvm.functions.** {*;} --keep class com.sun.jna.** { *; } --dontwarn java.awt.** --keep class * implements com.sun.jna.** { *; } --keep class * implements skip.bridge.** { *; } --keep class **._ModuleBundleAccessor_* { *; } --keep class pedi.foods.** { *; } diff --git a/pedi-foods/Android/app/src/main/AndroidManifest.xml b/pedi-foods/Android/app/src/main/AndroidManifest.xml deleted file mode 100644 index e0d85e6..0000000 --- a/pedi-foods/Android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/pedi-foods/Android/app/src/main/kotlin/Main.kt b/pedi-foods/Android/app/src/main/kotlin/Main.kt deleted file mode 100644 index c585807..0000000 --- a/pedi-foods/Android/app/src/main/kotlin/Main.kt +++ /dev/null @@ -1,227 +0,0 @@ -package pedi.foods - -import skip.lib.* -import skip.model.* -import skip.foundation.* -import skip.ui.* - -import android.Manifest -import android.app.Application -import android.graphics.Color as AndroidColor -import android.content.pm.PackageManager -import android.location.Location -import android.location.LocationManager -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.activity.SystemBarStyle -import androidx.activity.ComponentActivity -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.saveable.rememberSaveableStateHolder -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.luminance -import androidx.compose.ui.platform.LocalContext -import androidx.compose.material3.MaterialTheme -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat - -internal val logger: SkipLogger = SkipLogger(subsystem = "pedi.foods", category = "PediFoods") - -private typealias AppRootView = PediFoodsRootView -private typealias AppDelegate = PediFoodsAppDelegate - -/// AndroidAppMain is the `android.app.Application` entry point, and must match `application android:name` in the AndroidMainfest.xml file. -open class AndroidAppMain: Application { - constructor() { - } - - override fun onCreate() { - super.onCreate() - logger.info("starting app") - ProcessInfo.launch(applicationContext) - AppDelegate.shared.onInit() - } - - companion object { - } -} - -/// AndroidAppMain is initial `androidx.appcompat.app.AppCompatActivity`, and must match `activity android:name` in the AndroidMainfest.xml file. -open class MainActivity: AppCompatActivity { - private val locationRequestCode = 4201 - private val locationPermissionDeniedKey = "location_permission_denied" - private val locationLatitudeKey = "last_location_lat" - private val locationLongitudeKey = "last_location_lng" - - constructor() { - } - - override fun onCreate(savedInstanceState: android.os.Bundle?) { - super.onCreate(savedInstanceState) - logger.info("starting activity") - UIApplication.launch(this) - enableEdgeToEdge() - - setContent { - val saveableStateHolder = rememberSaveableStateHolder() - saveableStateHolder.SaveableStateProvider(true) { - PresentationRootView(ComposeContext()) - SideEffect { saveableStateHolder.removeState(true) } - } - } - - AppDelegate.shared.onLaunch() - requestLocationPermissionsIfNeeded() - } - - override fun onStart() { - logger.info("onStart") - super.onStart() - } - - override fun onResume() { - super.onResume() - AppDelegate.shared.onResume() - requestLocationPermissionsIfNeeded() - cacheLastKnownLocation() - } - - override fun onPause() { - super.onPause() - AppDelegate.shared.onPause() - } - - override fun onStop() { - super.onStop() - AppDelegate.shared.onStop() - } - - override fun onDestroy() { - super.onDestroy() - AppDelegate.shared.onDestroy() - } - - override fun onLowMemory() { - super.onLowMemory() - AppDelegate.shared.onLowMemory() - } - - override fun onRestart() { - logger.info("onRestart") - super.onRestart() - } - - override fun onSaveInstanceState(outState: android.os.Bundle): Unit = super.onSaveInstanceState(outState) - - override fun onRestoreInstanceState(bundle: android.os.Bundle) { - // Usually you restore your state in onCreate(). It is possible to restore it in onRestoreInstanceState() as well, but not very common. (onRestoreInstanceState() is called after onStart(), whereas onCreate() is called before onStart(). - logger.info("onRestoreInstanceState") - super.onRestoreInstanceState(bundle) - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: kotlin.Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - logger.info("onRequestPermissionsResult: ${requestCode}") - if (requestCode == locationRequestCode) { - val granted = grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED } - val prefs = getSharedPreferences("${packageName}_preferences", MODE_PRIVATE) - prefs.edit().putBoolean(locationPermissionDeniedKey, !granted).apply() - if (granted) { - cacheLastKnownLocation() - } - } - } - - private fun requestLocationPermissionsIfNeeded() { - val fine = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) - val coarse = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) - val granted = fine == PackageManager.PERMISSION_GRANTED || coarse == PackageManager.PERMISSION_GRANTED - if (granted) { - val prefs = getSharedPreferences("${packageName}_preferences", MODE_PRIVATE) - prefs.edit().putBoolean(locationPermissionDeniedKey, false).apply() - cacheLastKnownLocation() - return - } - - ActivityCompat.requestPermissions( - this, - kotlin.arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION), - locationRequestCode - ) - } - - private fun cacheLastKnownLocation() { - val manager = getSystemService(LOCATION_SERVICE) as? LocationManager ?: return - val fineGranted = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED - val coarseGranted = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED - if (!fineGranted && !coarseGranted) { - return - } - - val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER) - var best: Location? = null - for (provider in providers) { - val candidate = try { - manager.getLastKnownLocation(provider) - } catch (_: SecurityException) { - null - } catch (_: Exception) { - null - } - if (candidate != null && (best == null || candidate.accuracy < best.accuracy)) { - best = candidate - } - } - - if (best != null) { - val prefs = getSharedPreferences("${packageName}_preferences", MODE_PRIVATE) - prefs.edit() - .putString(locationLatitudeKey, best.latitude.toString()) - .putString(locationLongitudeKey, best.longitude.toString()) - .putBoolean(locationPermissionDeniedKey, false) - .apply() - } - } - - companion object { - } -} - -@Composable -internal fun SyncSystemBarsWithTheme() { - val dark = MaterialTheme.colorScheme.background.luminance() < 0.5f - - val transparent = AndroidColor.TRANSPARENT - val style = if (dark) { - SystemBarStyle.dark(transparent) - } else { - SystemBarStyle.light(transparent, transparent) - } - - val activity = LocalContext.current as? ComponentActivity - DisposableEffect(style) { - activity?.enableEdgeToEdge( - statusBarStyle = style, - navigationBarStyle = style - ) - onDispose { } - } -} - -@Composable -internal fun PresentationRootView(context: ComposeContext) { - val colorScheme = if (isSystemInDarkTheme()) ColorScheme.dark else ColorScheme.light - PresentationRoot(defaultColorScheme = colorScheme, context = context) { ctx -> - SyncSystemBarsWithTheme() - val contentContext = ctx.content() - Box(modifier = ctx.modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - AppRootView().Compose(context = contentContext) - } - } -} diff --git a/pedi-foods/Android/app/src/main/res/drawable-night/launch_splash.xml b/pedi-foods/Android/app/src/main/res/drawable-night/launch_splash.xml deleted file mode 100644 index 9e50909..0000000 --- a/pedi-foods/Android/app/src/main/res/drawable-night/launch_splash.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/pedi-foods/Android/app/src/main/res/drawable/launch_splash.xml b/pedi-foods/Android/app/src/main/res/drawable/launch_splash.xml deleted file mode 100644 index 5139fbd..0000000 --- a/pedi-foods/Android/app/src/main/res/drawable/launch_splash.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/pedi-foods/Android/app/src/main/res/drawable/placeholder_product.png b/pedi-foods/Android/app/src/main/res/drawable/placeholder_product.png deleted file mode 100644 index 889fa6c..0000000 Binary files a/pedi-foods/Android/app/src/main/res/drawable/placeholder_product.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/drawable/tracking_canceled.png b/pedi-foods/Android/app/src/main/res/drawable/tracking_canceled.png deleted file mode 100644 index 6b0acbc..0000000 Binary files a/pedi-foods/Android/app/src/main/res/drawable/tracking_canceled.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/drawable/tracking_completed.png b/pedi-foods/Android/app/src/main/res/drawable/tracking_completed.png deleted file mode 100644 index fee3d7f..0000000 Binary files a/pedi-foods/Android/app/src/main/res/drawable/tracking_completed.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/drawable/tracking_delivering.png b/pedi-foods/Android/app/src/main/res/drawable/tracking_delivering.png deleted file mode 100644 index 2d996ae..0000000 Binary files a/pedi-foods/Android/app/src/main/res/drawable/tracking_delivering.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/drawable/tracking_pending.png b/pedi-foods/Android/app/src/main/res/drawable/tracking_pending.png deleted file mode 100644 index a4a9344..0000000 Binary files a/pedi-foods/Android/app/src/main/res/drawable/tracking_pending.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/drawable/tracking_preparing.png b/pedi-foods/Android/app/src/main/res/drawable/tracking_preparing.png deleted file mode 100644 index 23fc9a7..0000000 Binary files a/pedi-foods/Android/app/src/main/res/drawable/tracking_preparing.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/drawable/tracking_ready.png b/pedi-foods/Android/app/src/main/res/drawable/tracking_ready.png deleted file mode 100644 index fe2f0f2..0000000 Binary files a/pedi-foods/Android/app/src/main/res/drawable/tracking_ready.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/pedi-foods/Android/app/src/main/res/mipmap-anydpi/ic_launcher.xml deleted file mode 100644 index c1ce612..0000000 --- a/pedi-foods/Android/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/pedi-foods/Android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/pedi-foods/Android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 0b02d33..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/pedi-foods/Android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png deleted file mode 100644 index acf6b7e..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/pedi-foods/Android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index 7454989..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/pedi-foods/Android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png deleted file mode 100644 index 0eeb46e..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/pedi-foods/Android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 221ce41..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/pedi-foods/Android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png deleted file mode 100644 index 0fdc22d..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/pedi-foods/Android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png deleted file mode 100644 index 5acc4e6..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/pedi-foods/Android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png deleted file mode 100644 index 3cf8fb1..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/pedi-foods/Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 61cdf75..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/pedi-foods/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png deleted file mode 100644 index bba635b..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/pedi-foods/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index 087580a..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png b/pedi-foods/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png deleted file mode 100644 index beb10dc..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/pedi-foods/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 97f2b6e..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/pedi-foods/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png deleted file mode 100644 index 2ee0546..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/pedi-foods/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 49e99f0..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/pedi-foods/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png deleted file mode 100644 index 7625d77..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/pedi-foods/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 8df98eb..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/pedi-foods/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png deleted file mode 100644 index adcde46..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/pedi-foods/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 8abb41e..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/pedi-foods/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png deleted file mode 100644 index a0175d9..0000000 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png and /dev/null differ diff --git a/pedi-foods/Android/app/src/main/res/values-night/themes.xml b/pedi-foods/Android/app/src/main/res/values-night/themes.xml deleted file mode 100644 index f59b9d2..0000000 --- a/pedi-foods/Android/app/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/pedi-foods/Android/app/src/main/res/values/colors.xml b/pedi-foods/Android/app/src/main/res/values/colors.xml deleted file mode 100644 index 6fd0990..0000000 --- a/pedi-foods/Android/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - #F6F7FB - #121212 - diff --git a/pedi-foods/Android/app/src/main/res/values/themes.xml b/pedi-foods/Android/app/src/main/res/values/themes.xml deleted file mode 100644 index fd28550..0000000 --- a/pedi-foods/Android/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/pedi-foods/Android/fastlane/Appfile b/pedi-foods/Android/fastlane/Appfile deleted file mode 100644 index 6f52e33..0000000 --- a/pedi-foods/Android/fastlane/Appfile +++ /dev/null @@ -1,11 +0,0 @@ -# This file contains the app distribution configuration -# for the Android half of the Skip app. -# You can find the documentation at https://docs.fastlane.tools - -# Load the shared Skip.env properties with the app info -require('dotenv') -Dotenv.load('../../Skip.env') -package_name(ENV['PRODUCT_BUNDLE_IDENTIFIER'].sub("-", "_")) - -# Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one -json_key_file("fastlane/apikey.json") diff --git a/pedi-foods/Android/fastlane/Fastfile b/pedi-foods/Android/fastlane/Fastfile deleted file mode 100644 index c71d4d5..0000000 --- a/pedi-foods/Android/fastlane/Fastfile +++ /dev/null @@ -1,51 +0,0 @@ -# This file contains the fastlane.tools configuration -# for the Android half of the Skip app. -# You can find the documentation at https://docs.fastlane.tools - -# Load the shared Skip.env properties with the app info -require('dotenv') -Dotenv.load '../../Skip.env' - -default_platform(:android) - -# use the Homebrew gradle rather than expecting a local gradlew -gradle_bin = (ENV['HOMEBREW_PREFIX'] ? ENV['HOMEBREW_PREFIX'] : "/opt/homebrew") + "/bin/gradle" - -default_platform(:android) - -desc "Build Skip Android App" -lane :build do |options| - build_config = (options[:release] ? "Release" : "Debug") - gradle( - task: "build${build_config}", - gradle_path: gradle_bin, - flags: "--warning-mode none -x lint" - ) -end - -desc "Test Skip Android App" -lane :test do - gradle( - task: "test", - gradle_path: gradle_bin - ) -end - -desc "Assemble Skip Android App" -lane :assemble do - gradle( - gradle_path: gradle_bin, - task: "bundleRelease" - ) - # sh "your_script.sh" -end - -desc "Deploy Skip Android App to Google Play" -lane :release do - - assemble - - upload_to_play_store( - aab: '../.build/Android/app/outputs/bundle/release/app-release.aab' - ) -end diff --git a/pedi-foods/Android/fastlane/metadata/android/en-US/full_description.txt b/pedi-foods/Android/fastlane/metadata/android/en-US/full_description.txt deleted file mode 100644 index c65ef9c..0000000 --- a/pedi-foods/Android/fastlane/metadata/android/en-US/full_description.txt +++ /dev/null @@ -1 +0,0 @@ -A great new app built with Skip! diff --git a/pedi-foods/Android/fastlane/metadata/android/en-US/short_description.txt b/pedi-foods/Android/fastlane/metadata/android/en-US/short_description.txt deleted file mode 100644 index c65ef9c..0000000 --- a/pedi-foods/Android/fastlane/metadata/android/en-US/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -A great new app built with Skip! diff --git a/pedi-foods/Android/gradle.properties b/pedi-foods/Android/gradle.properties deleted file mode 100644 index a175fe4..0000000 --- a/pedi-foods/Android/gradle.properties +++ /dev/null @@ -1,6 +0,0 @@ -org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -org.gradle.workers.max=2 -org.gradle.parallel=false -kotlin.daemon.jvmargs=-Xmx1g -android.useAndroidX=true -kotlin.code.style=official diff --git a/pedi-foods/Android/gradle/wrapper/gradle-wrapper.properties b/pedi-foods/Android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 630c082..0000000 --- a/pedi-foods/Android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1 +0,0 @@ -distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip diff --git a/pedi-foods/Android/settings.gradle.kts b/pedi-foods/Android/settings.gradle.kts deleted file mode 100644 index e20f3b3..0000000 --- a/pedi-foods/Android/settings.gradle.kts +++ /dev/null @@ -1,25 +0,0 @@ -// This gradle project is part of a conventional Skip app project. -pluginManagement { - // Initialize the Skip plugin folder and perform a pre-build for non-Xcode builds - val pluginPath = File.createTempFile("skip-plugin-path", ".tmp") - - // overriding outputs for an Android IDE can be done by un-commenting and setting the Xcode path: - //System.setProperty("BUILT_PRODUCTS_DIR", "${System.getProperty("user.home")}/Library/Developer/Xcode/DerivedData/MySkipProject-HASH/Build/Products/Debug-iphonesimulator") - - val skipPluginResult = providers.exec { - commandLine("/bin/sh", "-c", "skip plugin --prebuild --package-path '${settings.rootDir.parent}' --plugin-ref '${pluginPath.absolutePath}'") - environment("PATH", "${System.getenv("PATH")}:/opt/homebrew/bin") - } - val skipPluginOutput = skipPluginResult.standardOutput.asText.get() - print(skipPluginOutput) - val skipPluginError = skipPluginResult.standardError.asText.get() - print(skipPluginError) - - includeBuild(pluginPath.readText()) { - name = "skip-plugins" - } -} - -plugins { - id("skip-plugin") apply true -} diff --git a/pedi-foods/Darwin/Assets.xcassets/pedifoods.imageset/pedifoods.png b/pedi-foods/Darwin/Assets.xcassets/pedifoods.imageset/pedifoods.png deleted file mode 100644 index a94a4ff..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/pedifoods.imageset/pedifoods.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/pedifoods.imageset/pedifoods_dark.png b/pedi-foods/Darwin/Assets.xcassets/pedifoods.imageset/pedifoods_dark.png deleted file mode 100644 index a94a4ff..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/pedifoods.imageset/pedifoods_dark.png and /dev/null differ diff --git a/pedi-foods/Darwin/fastlane/metadata/en-US/title.txt b/pedi-foods/Darwin/fastlane/metadata/en-US/title.txt deleted file mode 100644 index 968e303..0000000 --- a/pedi-foods/Darwin/fastlane/metadata/en-US/title.txt +++ /dev/null @@ -1 +0,0 @@ -PediFoods diff --git a/pedi-foods/README.md b/pedi-foods/README.md deleted file mode 100644 index c663e6a..0000000 --- a/pedi-foods/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# PediFoods - -This is a [Skip](https://skip.tools) dual-platform app project. - - - - - - -## Building - -This project is both a stand-alone Swift Package Manager module, -as well as an Xcode project that builds and translates the project -into a Kotlin Gradle project for Android using the skipstone plugin. - -Building the module requires that Skip be installed using -[Homebrew](https://brew.sh) with `brew install skiptools/skip/skip`. - -This will also install the necessary Skip prerequisites: -Kotlin, Gradle, and the Android build tools. - -Installation prerequisites can be confirmed by running -`skip checkup`. The project can be validated with `skip verify`. - -## Running - -Xcode and Android Studio must be downloaded and installed in order to -run the app in the iOS simulator / Android emulator. -An Android emulator must already be running, which can be launched from -Android Studio's Device Manager. - -The project can be opened and run in Xcode from -`Project.xcworkspace`, which also enabled parallel -development of any Skip libary dependencies. - -To run both the Swift and Kotlin apps simultaneously, -launch the "PediFoods App" target from Xcode. -A build phases runs the "Launch Android APK" script that -will deploy the Skip app to a running Android emulator or connected device. -Logging output for the iOS app can be viewed in the Xcode console, and in -Android Studio's logcat tab for the transpiled Kotlin app, or -using `adb logcat` from a terminal. - -## Testing - -The module can be tested using the standard `swift test` command -or by running the test target for the macOS destination in Xcode, -which will run the Swift tests as well as the transpiled -Kotlin JUnit tests in the Robolectric Android simulation environment. - -Parity testing can be performed with `skip test`, -which will output a table of the test results for both platforms. - -Autor: Daniel Arantes Loverde diff --git a/pedi-foods/Sources/PediFoods/Resources/alelocard_logo.png b/pedi-foods/Sources/PediFoods/Resources/alelocard_logo.png deleted file mode 100644 index 9201194..0000000 Binary files a/pedi-foods/Sources/PediFoods/Resources/alelocard_logo.png and /dev/null differ diff --git a/pedi-foods/Sources/PediFoods/Resources/amexcard_logo.png b/pedi-foods/Sources/PediFoods/Resources/amexcard_logo.png deleted file mode 100644 index 42edc89..0000000 Binary files a/pedi-foods/Sources/PediFoods/Resources/amexcard_logo.png and /dev/null differ diff --git a/pedi-foods/Sources/PediFoods/Resources/hipercard_logo.png b/pedi-foods/Sources/PediFoods/Resources/hipercard_logo.png deleted file mode 100644 index a184c60..0000000 Binary files a/pedi-foods/Sources/PediFoods/Resources/hipercard_logo.png and /dev/null differ diff --git a/pedi-foods/Sources/PediFoods/Resources/icon_pix.png b/pedi-foods/Sources/PediFoods/Resources/icon_pix.png deleted file mode 100644 index d3d4824..0000000 Binary files a/pedi-foods/Sources/PediFoods/Resources/icon_pix.png and /dev/null differ diff --git a/pedi-foods/Sources/PediFoods/Resources/login_dark.png b/pedi-foods/Sources/PediFoods/Resources/login_dark.png deleted file mode 100644 index bf637d4..0000000 Binary files a/pedi-foods/Sources/PediFoods/Resources/login_dark.png and /dev/null differ diff --git a/pedi-foods/Sources/PediFoods/Resources/login_light.png b/pedi-foods/Sources/PediFoods/Resources/login_light.png deleted file mode 100644 index e7c5b09..0000000 Binary files a/pedi-foods/Sources/PediFoods/Resources/login_light.png and /dev/null differ diff --git a/pedi-foods/Sources/PediFoods/Resources/mastercard_logo.png b/pedi-foods/Sources/PediFoods/Resources/mastercard_logo.png deleted file mode 100644 index fefd01b..0000000 Binary files a/pedi-foods/Sources/PediFoods/Resources/mastercard_logo.png and /dev/null differ diff --git a/pedi-foods/Sources/PediFoods/Resources/pedifoods.png b/pedi-foods/Sources/PediFoods/Resources/pedifoods.png deleted file mode 100644 index a94a4ff..0000000 Binary files a/pedi-foods/Sources/PediFoods/Resources/pedifoods.png and /dev/null differ diff --git a/pedi-foods/Sources/PediFoods/Resources/pedifoods_dark.png b/pedi-foods/Sources/PediFoods/Resources/pedifoods_dark.png deleted file mode 100644 index a94a4ff..0000000 Binary files a/pedi-foods/Sources/PediFoods/Resources/pedifoods_dark.png and /dev/null differ diff --git a/pedi-foods/Sources/PediFoods/Resources/pin_image_app.png b/pedi-foods/Sources/PediFoods/Resources/pin_image_app.png deleted file mode 100644 index 7ed510e..0000000 Binary files a/pedi-foods/Sources/PediFoods/Resources/pin_image_app.png and /dev/null differ diff --git a/pedi-foods/Sources/PediFoods/Resources/pin_image_app_dark.png b/pedi-foods/Sources/PediFoods/Resources/pin_image_app_dark.png deleted file mode 100644 index 450d7bc..0000000 Binary files a/pedi-foods/Sources/PediFoods/Resources/pin_image_app_dark.png and /dev/null differ diff --git a/pedi-foods/Sources/PediFoods/Resources/sodexo_logo.png b/pedi-foods/Sources/PediFoods/Resources/sodexo_logo.png deleted file mode 100644 index 1aefb69..0000000 Binary files a/pedi-foods/Sources/PediFoods/Resources/sodexo_logo.png and /dev/null differ diff --git a/pedi-foods/Sources/PediFoods/Resources/visacard_logo.png b/pedi-foods/Sources/PediFoods/Resources/visacard_logo.png deleted file mode 100644 index 279c267..0000000 Binary files a/pedi-foods/Sources/PediFoods/Resources/visacard_logo.png and /dev/null differ diff --git a/pedifoods_app_plan.md b/pedifoods_app_plan.md deleted file mode 100644 index 21a601a..0000000 --- a/pedifoods_app_plan.md +++ /dev/null @@ -1,135 +0,0 @@ -# Plano de implementacao do app PediFoods (Skip + SwiftUI) - -## Legenda -- `[🟥]` Nao feito -- `[🟨]` Parcial -- `[🟩]` Completo - -## Estado atual (snapshot) -- `[🟩]` Base Skip + SwiftUI criada e compilando com fluxos `auth` e `main`. -- `[🟩]` Design system ativo com paleta, tipografia (Plus Jakarta Sans), raios e sombras. -- `[🟨]` Autenticacao com fluxo funcional de login por email/OTP (integracao inicial de API) e cadastro com navegacao pronta. -- `[🟨]` Home refeita com layout de referencia (header verde, categorias, cards, specials) e efeito de colapso em iOS. -- `[🟩]` Estado global (`AppState`) em uso para sessao, perfil, endereco, carrinho e favoritos. -- `[🟩]` API client/service com envelopes, retry/backoff, tratamento de erros/rate limit, politica de sessao expirada e persistencia segura de JWT no iOS. -- `[🟨]` Perfil com redesign visual alinhado a referencia, header no topo, lista de atalhos e confirmacao de logout. -- `[🟨]` Enderecos com lista real, criacao, edicao (reuso da tela de novo endereco) e exclusao por swipe compativel com Android. - -## Premissas de design e navegacao -- A tabbar e o centro de navegacao. Toda navegacao que nao for modal/over-context acontece dentro da tabbar. -- Login e cadastro ficam fora da tabbar. Ao logar/cadastrar, a Home abre ja com as tabs ativas. -- O verde da imagem `home_tabs.png` e o primary. O verde fluorescente sera usado raramente e fica como cor terciaria. -- Tipografia base: Plus Jakarta Sans. - -## Etapa 1 — Fundacao tecnica -- `[🟩]` Etapa geral -1. `[🟩]` Projeto Skip e estrutura base. -2. `[🟩]` Design system (cores/tipo/layout). -3. `[🟩]` Service base de API. -- `[🟩]` Feito: camada HTTP, headers padrao, tokens por modulo, tratamento de HTTP e 429, retry/backoff para falhas transientes, decode de erro com payload, politica de sessao expirada (`401/403`), limpeza de token em expiracao, persistencia de JWT em Keychain (iOS). -4. `[🟩]` Coordinator de navegacao. -- `[🟩]` Feito: fluxo `auth` -> `main`, tabbar com Home/Cart/Orders/Profile. -- `[🟩]` Feito: base de modais over-context (address picker e filtros) via `sheet` centralizado. -5. `[🟩]` State global (MVP). -6. `[🟩]` Permissions e GPS. -- `[🟩]` Feito: permissao/obter localizacao no iOS via `CoreLocation`, fallback de UX quando localizacao nao e obtida. -- `[🟩]` Feito: permissao de localizacao no Android (manifest + runtime request), cache de ultima localizacao valida e fallback para indisponivel/negado. - -## Etapa 2 — Autenticacao (fora da tabbar) -- `[🟨]` Etapa geral -1. `[🟩]` Tela Login (MVP). -2. `[🟩]` Tela Cadastro (MVP UI). -3. `[🟨]` Tela OTP (8 digitos). -- `[🟩]` Feito: fluxo OTP integrado no login por email e telefone. -- `[🟥]` Pendente: UX final de OTP em 8 digitos conforme referencia final. -4. `[🟨]` Fluxo pos-login. -- `[🟩]` Feito: login valida token e entra no fluxo principal. -- `[🟥]` Pendente: buscar perfil completo e sincronizar estado antes da Home. - -## Etapa 3 — Home e listagem de lojas (Tab Home) -- `[🟨]` Etapa geral -1. `[🟩]` Home com header, busca e categorias (MVP visual). -2. `[🟨]` Listagem de lojas via API `/api/app/stores`. -- `[🟩]` Feito: metodo de service pronto. -- `[🟥]` Pendente: bind real da Home com dados de API (atualmente usa mocks visuais). -3. `[🟨]` Filtros e busca. -- `[🟩]` Feito: UI base de busca/categorias e gatilho de modal de filtros. -- `[🟥]` Pendente: filtros reais + query server-side. -4. `[🟨]` Favoritos. -- `[🟩]` Feito: estado e affordance visual no card. -- `[🟥]` Pendente: persistencia/sync com backend. - -## Etapa 4 — Store Detail e Catalogo -- `[🟥]` Etapa geral -1. `[🟥]` Tela de detalhes da loja. -2. `[🟥]` Catalogo por categorias. -3. `[🟥]` Produtos com addons. -4. `[🟥]` Favoritos de loja. - -## Etapa 5 — Produto e carrinho -- `[🟥]` Etapa geral -1. `[🟥]` Tela de produto (detalhes + adicionais). -2. `[🟨]` Carrinho (Tab Cart). -- `[🟩]` Feito: tab e tela placeholder. -- `[🟥]` Pendente: itens reais, regra de negocio e estados. -3. `[🟥]` Quantidade e subtotal. - -## Etapa 6 — Checkout e pagamento -- `[🟥]` Etapa geral -1. `[🟥]` Tela de checkout. -2. `[🟥]` Endereco de entrega. -3. `[🟥]` Metodos de pagamento (PIX/credito/debito/dinheiro). -4. `[🟥]` Criacao de pedido. -5. `[🟥]` Fluxo de PIX (payload e QR). - -## Etapa 7 — Pedidos e rastreio -- `[🟥]` Etapa geral -1. `[🟨]` Lista de pedidos. -- `[🟨]` Feito: tab Orders criada + tela placeholder. -- `[🟥]` Pendente: lista real, detalhe e tracking. -2. `[🟥]` Detalhe do pedido. -3. `[🟥]` Tracking de status. - -## Etapa 8 — Perfil e enderecos -- `[🟨]` Etapa geral -1. `[🟨]` Perfil do usuario. -- `[🟩]` Feito: tela refeita com layout de referencia, header no topo, itens de menu, CTA de logout e confirmacao por alert. -- `[🟥]` Pendente: edicao completa de perfil e dados 100% vindos/sincronizados da API. -2. `[🟩]` Lista de enderecos. -- `[🟩]` Feito: carregamento de enderecos, selecao, estado principal e persistencia local da selecao. -3. `[🟩]` Criar/editar/excluir endereco. -- `[🟩]` Feito: criacao, edicao reutilizando a mesma tela e exclusao por swipe com compatibilidade Android. - -## Etapa 9 — Mapa e GPS -- `[🟥]` Etapa geral -1. `[🟥]` Mapa iOS via MapKit. -2. `[🟥]` Mapa Android via ComposeView (Google Maps). -3. `[🟥]` Pinos de loja e localizacao atual. -4. `[🟨]` Ajustes de permissao e fallback sem GPS. - -## Etapa 10 — Polimento e release -- `[🟥]` Etapa geral -1. `[🟨]` Dark mode (presente em partes de auth). -2. `[🟨]` Estados vazios e erros. -3. `[🟥]` Performance e cache. -4. `[🟥]` Build iOS e Android (ha bloqueio atual de dependencia `LCEssentials` em `StoreDetailView.swift`). -5. `[🟥]` Export APK/AAB. - -## Etapa 11 — Ajustes de UI (referencias) -- `[🟨]` Etapa geral -1. `[🟨]` Login (light/dark). -2. `[🟨]` Cadastro. -3. `[🟨]` OTP. -4. `[🟥]` Carrinho. -5. `[🟥]` Checkout. -6. `[🟨]` Perfil. -7. `[🟨]` Pedidos. - -## Proximos focos recomendados -1. `[🟨]` Conectar Home com API real (`stores`, busca, categoria, favoritos). -2. `[🟨]` Fechar fluxo auth pos-login (perfil + hidratacao de estado). -3. `[🟥]` Implementar carrinho real (itens, subtotal, edicao) para destravar checkout. -4. `[🟥]` Implementar mapa e tracking de pedidos. -5. `[🟥]` Corrigir bloqueio de build por dependencia (`LCEssentials`) para fechar validacao iOS/Android ponta a ponta. - -Autor: Daniel Arantes Loverde diff --git a/pedifoods_system_design.md b/pedifoods_system_design.md deleted file mode 100644 index 9088ffd..0000000 --- a/pedifoods_system_design.md +++ /dev/null @@ -1,183 +0,0 @@ -# 📦 Design System — Green Food Delivery App - -Este documento define o **design system oficial** do webapp de food delivery, baseado em uma identidade visual **fresh, health-focused e energética**, com forte uso de verde neon e contrastes suaves. - -Este arquivo deve ser usado como **prompt base canônico** para qualquer IA, designer ou desenvolvedor. - ---- - -## 1. FUNDAMENTOS VISUAIS - -### 1.1 🎨 Paleta de Cores - -#### Cores Principais - -```ts -colors: { - primary: "#388E3C", // ação principal, CTAs, destaque - backgroundLight: "#F7F8F5", - backgroundDark: "#18230F", - textPrimary: "#FFFFFF", - textInverse: "#333333" -} -``` - -Uso: -- Primary → botões principais, ícones ativos, ratings -- Secondary → headers, botões secundários -- BackgroundLight → fundo global -- BackgroundDark → dark mode - ---- - ---- - -### 1.3 🔤 Tipografia - -Fonte oficial: -- **Plus Jakarta Sans** - -```ts -typography: { - fontFamily: "'Plus Jakarta Sans', sans-serif", - - heading1: { - size: "30px", - weight: 800 - }, - - heading2: { - size: "22px", - weight: 800 - }, - - body: { - size: "16px", - weight: 400, - lineHeight: "0" - }, - - button: { - size: "14px", - weight: 800, - transform: "uppercase", - letterSpacing: "0.08em" - }, - - caption: { - size: "10px", - weight: 700, - letterSpacing: "0.12em" - } -} -``` - ---- - -### 1.4 📐 Bordas, Espaçamento e Elevação - -```ts -radius: { - md: "0.75rem", - lg: "1rem", - xl: "1.5rem", - full: "9999px" -} - -spacing: [4, 8, 12, 16, 24, 32] - -shadow: { - soft: "0 4px 20px -2px rgba(0,0,0,0.05)", - glow: "0 8px 24px rgba(140,255,46,0.2)" -} -``` - ---- - -## 2. COMPONENTES - -### 2.1 🔘 Botões - -#### Primary Action Button -- Fundo: primary -- Texto: secondary -- Altura: 56px -- Radius: lg -- Sombra glow -- Feedback de escala - -```ts -Button { - variant: "primary" | "secondary" - fullWidth: boolean -} -``` - ---- - -### 2.2 🔍 Input & Search - -- Fundo claro -- Ícone à esquerda -- Sem borda -- Focus ring em primary - -```ts -SearchInput { - placeholder: string - icon: "search" -} -``` - ---- - -### 2.3 🃏 Food Card - -Contém: -- Imagem hero -- Botão de favorito flutuante -- Nome -- Rating -- Tempo estimado - -```ts -FoodCard { - title: string - image: string - rating: number - timeRange: string - favorite: boolean -} -``` - ---- - -### 2.4 🧭 Navigation Bar - -- Fixa no rodapé -- Ícones Material Symbols -- Item ativo em primary - -```ts -TabBar { - items: TabItem[] - activeIndex: number -} -``` - ---- - -### 2.5 🧩 Ícones - -- Material Symbols Outlined -- Peso visual leve -- Cor semântica - -```ts -Icon { - name: string - active?: boolean -} -``` - -Autor: Daniel Arantes Loverde diff --git a/reference_img/IMG_0442.PNG b/reference_img/IMG_0442.PNG deleted file mode 100644 index 36c8b33..0000000 Binary files a/reference_img/IMG_0442.PNG and /dev/null differ diff --git a/reference_img/IMG_0443.PNG b/reference_img/IMG_0443.PNG deleted file mode 100644 index 861e482..0000000 Binary files a/reference_img/IMG_0443.PNG and /dev/null differ diff --git a/reference_img/caracters/canceled_order.png b/reference_img/caracters/canceled_order.png deleted file mode 100644 index 3348e9e..0000000 Binary files a/reference_img/caracters/canceled_order.png and /dev/null differ diff --git a/reference_img/caracters/converted/tracking-canceled.png b/reference_img/caracters/converted/tracking-canceled.png deleted file mode 100644 index 6b0acbc..0000000 Binary files a/reference_img/caracters/converted/tracking-canceled.png and /dev/null differ diff --git a/reference_img/caracters/converted/tracking-completed.png b/reference_img/caracters/converted/tracking-completed.png deleted file mode 100644 index fee3d7f..0000000 Binary files a/reference_img/caracters/converted/tracking-completed.png and /dev/null differ diff --git a/reference_img/caracters/converted/tracking-delivering.png b/reference_img/caracters/converted/tracking-delivering.png deleted file mode 100644 index 2d996ae..0000000 Binary files a/reference_img/caracters/converted/tracking-delivering.png and /dev/null differ diff --git a/reference_img/caracters/converted/tracking-pending.png b/reference_img/caracters/converted/tracking-pending.png deleted file mode 100644 index a4a9344..0000000 Binary files a/reference_img/caracters/converted/tracking-pending.png and /dev/null differ diff --git a/reference_img/caracters/converted/tracking-preparing.png b/reference_img/caracters/converted/tracking-preparing.png deleted file mode 100644 index 23fc9a7..0000000 Binary files a/reference_img/caracters/converted/tracking-preparing.png and /dev/null differ diff --git a/reference_img/caracters/converted/tracking-ready.png b/reference_img/caracters/converted/tracking-ready.png deleted file mode 100644 index fe2f0f2..0000000 Binary files a/reference_img/caracters/converted/tracking-ready.png and /dev/null differ diff --git a/reference_img/caracters/delivered_order.png b/reference_img/caracters/delivered_order.png deleted file mode 100644 index 0b5ad40..0000000 Binary files a/reference_img/caracters/delivered_order.png and /dev/null differ diff --git a/reference_img/caracters/delivering_order.png b/reference_img/caracters/delivering_order.png deleted file mode 100644 index f3291ad..0000000 Binary files a/reference_img/caracters/delivering_order.png and /dev/null differ diff --git a/reference_img/caracters/pending_order.png b/reference_img/caracters/pending_order.png deleted file mode 100644 index 2db91b0..0000000 Binary files a/reference_img/caracters/pending_order.png and /dev/null differ diff --git a/reference_img/caracters/preparing_order.png b/reference_img/caracters/preparing_order.png deleted file mode 100644 index 6e466b5..0000000 Binary files a/reference_img/caracters/preparing_order.png and /dev/null differ diff --git a/reference_img/caracters/ready_order.png b/reference_img/caracters/ready_order.png deleted file mode 100644 index 44b72eb..0000000 Binary files a/reference_img/caracters/ready_order.png and /dev/null differ diff --git a/reference_img/cart.png b/reference_img/cart.png deleted file mode 100644 index cccfac5..0000000 Binary files a/reference_img/cart.png and /dev/null differ diff --git a/reference_img/cart_checkout.png b/reference_img/cart_checkout.png deleted file mode 100644 index 9016bde..0000000 Binary files a/reference_img/cart_checkout.png and /dev/null differ diff --git a/reference_img/category_all.png b/reference_img/category_all.png deleted file mode 100644 index 031c9bf..0000000 Binary files a/reference_img/category_all.png and /dev/null differ diff --git a/reference_img/create_account.png b/reference_img/create_account.png deleted file mode 100644 index d9e25ec..0000000 Binary files a/reference_img/create_account.png and /dev/null differ diff --git a/reference_img/cuppons.png b/reference_img/cuppons.png deleted file mode 100644 index 049f142..0000000 Binary files a/reference_img/cuppons.png and /dev/null differ diff --git a/reference_img/home_tabs.png b/reference_img/home_tabs.png deleted file mode 100644 index 77434b7..0000000 Binary files a/reference_img/home_tabs.png and /dev/null differ diff --git a/reference_img/login.png b/reference_img/login.png deleted file mode 100644 index 49f770c..0000000 Binary files a/reference_img/login.png and /dev/null differ diff --git a/reference_img/login_dark.png b/reference_img/login_dark.png deleted file mode 100644 index bf637d4..0000000 Binary files a/reference_img/login_dark.png and /dev/null differ diff --git a/reference_img/login_light.png b/reference_img/login_light.png deleted file mode 100644 index e7c5b09..0000000 Binary files a/reference_img/login_light.png and /dev/null differ diff --git a/reference_img/my_address.png b/reference_img/my_address.png deleted file mode 100644 index d86056e..0000000 Binary files a/reference_img/my_address.png and /dev/null differ diff --git a/reference_img/my_favorites.png b/reference_img/my_favorites.png deleted file mode 100644 index 809438d..0000000 Binary files a/reference_img/my_favorites.png and /dev/null differ diff --git a/reference_img/my_orders.png b/reference_img/my_orders.png deleted file mode 100644 index 4c08b86..0000000 Binary files a/reference_img/my_orders.png and /dev/null differ diff --git a/reference_img/my_orders_detail.png b/reference_img/my_orders_detail.png deleted file mode 100644 index 9964d74..0000000 Binary files a/reference_img/my_orders_detail.png and /dev/null differ diff --git a/reference_img/my_profile.png b/reference_img/my_profile.png deleted file mode 100644 index b70b325..0000000 Binary files a/reference_img/my_profile.png and /dev/null differ diff --git a/reference_img/my_reviews.png b/reference_img/my_reviews.png deleted file mode 100644 index 3d951bb..0000000 Binary files a/reference_img/my_reviews.png and /dev/null differ diff --git a/reference_img/my_reviews_detail.png b/reference_img/my_reviews_detail.png deleted file mode 100644 index 25c282a..0000000 Binary files a/reference_img/my_reviews_detail.png and /dev/null differ diff --git a/reference_img/notifications.png b/reference_img/notifications.png deleted file mode 100644 index 581478f..0000000 Binary files a/reference_img/notifications.png and /dev/null differ diff --git a/reference_img/otp_validation.png b/reference_img/otp_validation.png deleted file mode 100644 index a142079..0000000 Binary files a/reference_img/otp_validation.png and /dev/null differ diff --git a/reference_img/payment_card.png b/reference_img/payment_card.png deleted file mode 100644 index 9ce174f..0000000 Binary files a/reference_img/payment_card.png and /dev/null differ diff --git a/reference_img/payment_pix.png b/reference_img/payment_pix.png deleted file mode 100644 index adeea2d..0000000 Binary files a/reference_img/payment_pix.png and /dev/null differ diff --git a/reference_img/pin_image_app.png b/reference_img/pin_image_app.png deleted file mode 100644 index 7ed510e..0000000 Binary files a/reference_img/pin_image_app.png and /dev/null differ diff --git a/reference_img/pin_image_app_dark.png b/reference_img/pin_image_app_dark.png deleted file mode 100644 index 450d7bc..0000000 Binary files a/reference_img/pin_image_app_dark.png and /dev/null differ diff --git a/reference_img/product_detail.png b/reference_img/product_detail.png deleted file mode 100644 index 2b38d7d..0000000 Binary files a/reference_img/product_detail.png and /dev/null differ diff --git a/reference_img/real_time_track_02.png b/reference_img/real_time_track_02.png deleted file mode 100644 index a308acf..0000000 Binary files a/reference_img/real_time_track_02.png and /dev/null differ diff --git a/reference_img/real_time_track_04.png b/reference_img/real_time_track_04.png deleted file mode 100644 index 08e3bf5..0000000 Binary files a/reference_img/real_time_track_04.png and /dev/null differ diff --git a/reference_img/reviewing.png b/reference_img/reviewing.png deleted file mode 100644 index 2e2be8f..0000000 Binary files a/reference_img/reviewing.png and /dev/null differ diff --git a/reference_img/search_filter.png b/reference_img/search_filter.png deleted file mode 100644 index 4275159..0000000 Binary files a/reference_img/search_filter.png and /dev/null differ diff --git a/reference_img/store_screen.png b/reference_img/store_screen.png deleted file mode 100644 index 229e69a..0000000 Binary files a/reference_img/store_screen.png and /dev/null differ diff --git a/reference_img/support.png b/reference_img/support.png deleted file mode 100644 index 107c88a..0000000 Binary files a/reference_img/support.png and /dev/null differ diff --git a/pedi-foods/scripts/android-run.sh b/scripts/android-run.sh similarity index 100% rename from pedi-foods/scripts/android-run.sh rename to scripts/android-run.sh diff --git a/skip_for_android_plan.md b/skip_for_android_plan.md deleted file mode 100644 index 5f40205..0000000 --- a/skip_for_android_plan.md +++ /dev/null @@ -1,363 +0,0 @@ -# Plano de Ação - Skip para Android - -## Objetivo - -Preparar o projeto `PediFoods` para o primeiro ciclo real de testes no Android, preservando integralmente o funcionamento atual do iOS. - -Regra principal deste plano: - -1. Todo ajuste necessário para Android deve ser isolado e não pode causar regressão no iOS. -2. Sempre que possível, a separação deve ser feita com condicionais por plataforma, abstrações compatíveis ou implementações específicas para Android. -3. Cada task concluída e validada deve gerar commit próprio. - -## Premissas confirmadas - -1. O projeto já possui base Skip dual-platform. -2. O iOS está funcional e deve ser preservado como referência de comportamento. -3. O Android já possui estrutura inicial pronta: - - `pedi-foods/Android/*` - - `pedi-foods/Android/app/src/main/kotlin/Main.kt` - - `pedi-foods/Android/app/src/main/AndroidManifest.xml` - - `pedi-foods/Sources/PediFoods/Skip/skip.yml` -4. O objetivo imediato não é publicar Android, e sim conseguir compilar, instalar, abrir e validar os fluxos principais. - -## Diagnóstico atual - -### 1. Estrutura Android existente - -O projeto já possui: - -1. Manifest com permissões de internet e localização. -2. `Main.kt` com tratamento inicial de permissão de localização no Android. -3. Script dedicado para exportar e rodar no Android: - - `pedi-foods/scripts/android-run.sh` -4. Recursos Android básicos como ícones launcher. - -### 2. Riscos identificados - -Os principais riscos para o primeiro teste Android são: - -1. Imports e dependências iOS-only ainda expostos em arquivos compartilhados. -2. Build local atualmente bloqueado por problema de artefato/toolchain Skip. -3. Recursos e comportamentos com paridade incompleta no Android. -4. Cobertura de testes ainda insuficiente para confiar só em automação. - -### 3. Bloqueios técnicos já identificados - -#### 3.1. Código compartilhado com dependência iOS-only - -Foram encontrados pontos com alto risco para Android: - -1. `pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView.swift` - - possui `import LCEssentials` sem blindagem de plataforma. -2. `pedi-foods/Sources/PediFoods/Views/Main/ProfileView.swift` - - possui `import LCEssentials` sem blindagem de plataforma. -3. Outros arquivos com dependências iOS-only exigem revisão cuidadosa: - - `pedi-foods/Sources/PediFoods/Views/Main/HomeView.swift` - - `pedi-foods/Sources/PediFoods/Views/Main/ReviewsView.swift` - - `pedi-foods/Sources/PediFoods/Views/Main/UserProfileView.swift` - - `pedi-foods/Sources/PediFoods/Services/ApiClient.swift` - -#### 3.2. Ambiente/build Skip inconsistente - -Na validação local, o comando de build falhou antes da compilação funcional: - -1. `swift build` retornou erro de artefato ausente do Skip. -2. O erro aponta para caminho antigo de projeto, indicando problema de cache, artefato ou configuração local do ambiente. - -Sem resolver isso, não é possível validar corretamente o Android. - -#### 3.3. Paridade Android ainda parcial - -Alguns comportamentos parecem estar deliberadamente simplificados para Android: - -1. Snackbar no Android está atualmente neutro/stubado. -2. Splash nativo Android ainda não aparenta estar finalizado. -3. Recursos visuais e alguns fluxos podem abrir, mas não com equivalência total ao iOS. - -Isso não impede o primeiro boot, mas entra como ajuste após a compilação inicial. - -## Estratégia de execução - -A execução deve seguir uma ordem rígida para evitar retrabalho: - -1. Primeiro remover bloqueios de compilação cross-platform. -2. Depois estabilizar o ambiente Skip/Android local. -3. Em seguida gerar build Android real. -4. Depois validar execução no device/emulador. -5. Só então ajustar bugs e diferenças de comportamento específicas do Android. -6. Ao fim de cada etapa funcional concluída, gerar commit. - -## Plano de ação detalhado - -### Fase 1 - Blindagem do código compartilhado para Android - -Objetivo: -Garantir que o código compartilhado consiga ser compilado para Android sem quebrar o iOS. - -Ações: - -1. Revisar todos os arquivos compartilhados com: - - `import LCEssentials` - - `import UIKit` - - `import PhotosUI` - - qualquer API não portável pelo Skip -2. Encapsular uso iOS-only com: - - `#if os(iOS)` - - `#if canImport(...)` - - abstrações seguras por plataforma -3. Confirmar que nenhum ajuste Android altere o comportamento já funcional do iOS. -4. Validar se os fallbacks Android são suficientes para compilar. - -Critério de conclusão: - -1. O código compartilhado deixa de possuir bloqueios óbvios de compilação Android. -2. As dependências iOS-only ficam totalmente isoladas. - -Commit ao concluir: - -1. Criar commit específico da blindagem cross-platform. - -### Fase 2 - Saneamento do ambiente Skip/Android - -Objetivo: -Restabelecer o pipeline de build para que a validação Android seja real e repetível. - -Ações: - -1. Identificar a origem do erro de artefato do Skip. -2. Verificar caches, artefatos e referências para caminho antigo do projeto. -3. Corrigir ambiente local sem destruir configuração válida do iOS. -4. Validar novamente: - - `swift build` - - fluxo de export Android via Skip - -Critério de conclusão: - -1. O build compartilhado executa sem erro de artefato. -2. O projeto consegue iniciar a etapa de export Android. - -Commit ao concluir: - -1. Criar commit específico do ajuste de ambiente/configuração, se houver mudança versionada no repositório. -2. Se a correção for apenas local e não versionável, registrar isso no andamento e seguir sem commit desta subparte. - -### Fase 3 - Primeiro build Android real - -Objetivo: -Gerar APK/build Android funcional para teste. - -Ações: - -1. Rodar export/build Android pelo fluxo oficial do projeto. -2. Validar geração dos artefatos Android esperados. -3. Confirmar compatibilidade com ABI do emulador/device. -4. Corrigir falhas de build especificamente Android sem alterar o caminho iOS. - -Critério de conclusão: - -1. APK ou build instalável gerado com sucesso. -2. Nenhum erro de compilação Android pendente. - -Commit ao concluir: - -1. Criar commit específico do primeiro build Android estabilizado. - -### Fase 4 - Primeiro teste funcional no Android - -Objetivo: -Instalar, abrir e validar o app no Android pela primeira vez. - -Ações: - -1. Instalar no emulador ou device. -2. Validar abertura do app. -3. Confirmar: - - splash inicial - - fluxo de login - - navegação principal por tabs - - home - - detalhe da loja - - perfil - - endereços -4. Observar crashes, telas em branco, problemas de navegação ou layout quebrado. - -Critério de conclusão: - -1. O app abre no Android. -2. Os fluxos principais navegam sem crash. - -Commit ao concluir: - -1. Criar commit específico do primeiro ciclo funcional de execução Android. - -### Fase 5 - Correções de runtime específicas do Android - -Objetivo: -Ajustar o que só aparece após o app abrir no Android. - -Ações: - -1. Validar permissão de localização. -2. Validar persistência/local cache usados pelo app no Android. -3. Validar imagens e carregamento remoto. -4. Validar teclado, foco e `adjustResize`. -5. Validar sheets, modais, overlays e navegação. -6. Corrigir bugs Android-only com isolamento por plataforma. - -Critério de conclusão: - -1. Os fluxos principais deixam de ter falhas críticas no Android. -2. Nenhuma correção Android impacta o iOS. - -Commit ao concluir: - -1. Criar commits separados por grupo funcional concluído e validado. - -### Fase 6 - Paridade mínima de UX Android - -Objetivo: -Atingir um nível mínimo aceitável de experiência para teste Android sem buscar perfeição visual prematura. - -Ações: - -1. Ajustar snackbar Android para comportamento visível e funcional. -2. Revisar splash Android nativo, se necessário. -3. Corrigir diferenças de layout mais críticas. -4. Revisar componentes com comportamento visual diferente entre plataformas. - -Critério de conclusão: - -1. O Android fica funcional e utilizável para teste contínuo. -2. As diferenças visuais restantes não impedem validação do produto. - -Commit ao concluir: - -1. Criar commit específico das melhorias de UX Android aprovadas. - -### Fase 7 - Regressão rápida no iOS - -Objetivo: -Garantir que os ajustes Android não introduziram regressões no iOS. - -Ações: - -1. Rodar build iOS novamente. -2. Validar navegação principal no iOS. -3. Confirmar que os fluxos já funcionais continuam intactos. -4. Revisar especialmente os pontos alterados com condicionais de plataforma. - -Critério de conclusão: - -1. iOS continua funcional após os ajustes Android. -2. Nenhuma regressão relevante é detectada. - -Commit ao concluir: - -1. Criar commit final de consolidação apenas se houver mudanças adicionais nesta etapa. - -## Regras operacionais durante a execução - -1. Nenhum arquivo pode ultrapassar 500 linhas. -2. Ajustes Android devem ser isolados e não devem reescrever a lógica funcional do iOS. -3. Toda correção deve passar por validação antes de seguir para a próxima task. -4. Cada task funcional concluída deve resultar em commit. -5. Se uma task encontrar erro, ela retorna ao ciclo: - - identificar causa raiz - - corrigir - - testar novamente - - só então marcar como concluída - -## Convenção de commits - -Padrão recomendado: - -1. `fix(android): isolate ios-only dependencies for skip build` -2. `fix(build): stabilize skip android export pipeline` -3. `feat(android): first successful android build` -4. `fix(android): resolve runtime issues on first boot` -5. `feat(android): improve minimum ux parity` - -## Sequência de tasks para acompanhamento - -Legenda: - -- 🔴 Pendente -- 🟢 Concluído - -1. 🟢 Revisar e blindar imports/dependências iOS-only no código compartilhado. -2. 🟢 Corrigir o problema de artefato/caminho do ambiente Skip local. -3. 🟢 Validar `swift build` sem erro de ambiente. -4. 🟢 Validar export/build Android via Skip. -5. 🟢 Gerar APK/build Android instalável. -6. 🟢 Instalar e abrir o app no Android. -7. 🔴 Validar login no Android. -8. 🔴 Validar home e navegação principal no Android. -9. 🔴 Validar detalhe de loja e carrinho no Android. -10. 🔴 Validar perfil e endereços no Android. -11. 🔴 Validar permissão e uso de localização no Android. -12. 🔴 Corrigir bugs críticos de runtime Android. -13. 🟢 Ajustar snackbar Android. -14. 🟢 Revisar splash Android, se necessário. -15. 🔴 Rodar regressão rápida no iOS após os ajustes Android. -16. 🟢 Criar commit após cada task concluída e funcional. - -## Andamento - -1. Task 1 concluída e validada via build compartilhado. -2. Task 2 concluída com ajuste local no cache/estado de build do Skip. - - Correção local não versionada. -3. Task 3 concluída com `swift build` finalizado com sucesso. -4. Task 4 concluída com `skip export --debug --no-ios --android --arch aarch64 --plain --verbose`. - - O `skip export` fechou com sucesso completo. - - O pipeline passou pela recompilação Swift compartilhada, geração Android Skip, `assembleDebug`, `bundleDebug` e export final. - - Artefatos gerados com sucesso em `pedi-foods/.build/skip-export/`: - - `PediFoods-debug.apk` - - `PediFoods-debug.aab` - - `PediFoods-project.zip` -5. Task 5 concluída com geração validada de artefatos Android instaláveis. - - APK exportado com sucesso. - - AAB exportado com sucesso. -6. Task 13 concluída com implementação funcional de snackbar no Android. - - `SnackbarCenter` Android deixou de ser stub e agora controla exibição, timeout e tap action. - - `SnackbarOverlay` Android passou a renderizar o banner visual e reagir a mudanças de estado. - - `swift build` validado com sucesso após a alteração. - - A validação `skip export` desta rodada ficou bloqueada por erro pré-existente em `Vendor/skip-fuse-ui` (`onGeometryChangeErased`) e em código gerado `SkipUI`, sem relação direta com a task de snackbar. -7. Ajuste incremental de paridade no perfil Android concluído. - - `UserProfileView+Android` agora mostra preview de avatar quando `profilePicture` está preenchido. - - Campo de edição de foto por URL/Base64 adicionado apenas no Android. - - `swift build` validado com sucesso após o ajuste. -8. Padronização de clipboard cross-platform concluída para fluxos OTP/PIX. - - Helper de clipboard centralizado em `PlatformCompat`. - - `OtpView` e `CheckoutView` passaram a usar o helper compartilhado. - - `swift build` validado com sucesso após o ajuste. -9. Ajuste de atualização de localização Android concluído em `Main.kt`. - - Atualização/permissão de localização acionada também no `onResume`. - - Removido uso de non-null assertion desnecessária no cálculo de melhor coordenada. - - `swift build` validado com sucesso. - - A validação Gradle completa continuou bloqueada por erro pré-existente em `SkipUI` gerado (`GroupBoxCompat.kt`), sem relação direta com `Main.kt`. -10. Task 14 concluída com splash nativo Android dedicado. - - `AndroidManifest` atualizado para usar tema de launch Android específico. - - Recursos adicionados para tema, cores e drawables (`values`, `values-night`, `drawable`, `drawable-night`). - - Validação de recursos Android concluída com sucesso em `gradle :app:processDebugResources --no-daemon`. -11. Ajuste de fallback do colapso de header na Home concluído para Android. - - `HomeScrollOffsetObserver` habilitado para uso cross-platform. - - `HomeView` deixou de usar `EmptyView()` no observer em Android. - - `swift build` validado com sucesso após o ajuste. -12. Correção de compatibilidade do bridge Android concluída para destravar export. - - `skip-fuse-ui` recebeu fallback Android-only para `onGeometryChange(...)`, evitando dependência de hook de geometria ausente no bridge. - - `skip-ui` recebeu shim Android-only para `GroupBox`, mantendo iOS preservado. - - Ajustes feitos no vendor local sem alterar comportamento iOS. -13. Compatibilidade de `coordinateSpace` aplicada com fallback Android. - - Criado `appNamedCoordinateSpace(...)` em `PlatformCompat`. - - `HomeView` e `StoreDetailView` passaram a usar o helper compatível. - - O fluxo iOS permanece no caminho original (`coordinateSpace`) e Android usa no-op seguro. -14. Export Android final validado com sucesso. - - `skip export --debug --no-ios --android --arch aarch64 --plain --verbose` concluído com sucesso. - - Artefatos Android gerados novamente em `pedi-foods/.build/skip-export`. -15. Execução direta (`skip android run`) compilou com sucesso, porém sem instalação por ausência de device conectado. - - Build Swift/Kotlin finalizado. - - Etapa de conexão Android falhou com `adb devices` vazio no ambiente atual. - - Próximo passo para concluir Task 6: conectar device físico (USB) ou iniciar AVD válido. - -Autor: Daniel Arantes Loverde diff --git a/subdomain_stores_plan.md b/subdomain_stores_plan.md deleted file mode 100644 index e0f274c..0000000 --- a/subdomain_stores_plan.md +++ /dev/null @@ -1,746 +0,0 @@ -# Subdomain Stores Plan - -## Objetivo - -Reorganizar o ecossistema do PediFoods para um modelo claro de subdomínios, mantendo a integração com o backend do Atomenta onde ela faz sentido, mas separando melhor identidade, navegação e contexto de cada área. - -## Cenário Desejado - -### 1. `pedifoods.com.br` - -Papel: -- home pública -- landing page -- login -- onboarding inicial -- após login, exibir lista de lojas do usuário - -Responsabilidade: -- identidade pública do PediFoods -- autenticação do usuário final/parceiro -- seleção da loja - -### 2. `parceiro.pedifoods.com.br` - -Papel: -- painel do lojista -- equivalente ao atual "Atomenta Store" -- gestão operacional da loja - -Responsabilidade: -- backoffice do parceiro -- configuração da loja -- cardápio -- pedidos -- financeiro -- onboarding do parceiro - -### 3. `nome-curto-loja.pedifoods.com.br` - -Papel: -- storefront da loja -- vitrine pública ou semi-logada da loja -- produtos, categorias, carrinho e experiência do cliente - -Responsabilidade: -- experiência da loja individual -- branding e contexto por loja -- leitura do catálogo - -## Princípio Arquitetural - -O PediFoods não deve mais parecer "um Atomenta renomeado". - -Ele pode continuar: -- consumindo APIs do Atomenta -- reutilizando lógica de OTP -- reutilizando cookies/tokens e integração segura quando necessário - -Mas deve deixar claro: -- qual domínio é PediFoods -- qual domínio é painel parceiro -- qual domínio é storefront da loja - -## Direção Técnica - -### Separar 3 contextos - -1. `public app` -- domínio base `pedifoods.com.br` -- homepage, login, seleção de lojas - -2. `partner app` -- subdomínio fixo `parceiro.pedifoods.com.br` -- painel do lojista - -3. `store app` -- subdomínio dinâmico `{slug}.pedifoods.com.br` -- storefront da loja - -### Manter 1 base de código no curto prazo - -No curto prazo, o melhor custo-benefício é manter uma única codebase do PediFoods com roteamento por host. - -Ou seja: -- o app lê o `Host` -- identifica o contexto -- renderiza a área correta - -Exemplo de resolução: -- `pedifoods.com.br` => contexto `public` -- `parceiro.pedifoods.com.br` => contexto `partner` -- `slug.pedifoods.com.br` => contexto `store` - -Isso evita: -- duplicação de projeto -- deploy múltiplo desnecessário -- divergência de regra de negócio - -## Regras de Resolução de Host - -### Contexto `public` - -Hosts: -- `pedifoods.com.br` -- `www.pedifoods.com.br` se existir - -Fluxos: -- home -- login -- cadastro -- recuperação de conta -- listagem de lojas do usuário autenticado - -### Contexto `partner` - -Host: -- `parceiro.pedifoods.com.br` - -Fluxos: -- login do parceiro -- dashboard da loja -- onboarding -- catálogo -- pedidos -- financeiro - -### Contexto `store` - -Host: -- `{storeSlug}.pedifoods.com.br` - -Fluxos: -- catálogo público da loja -- detalhes da loja -- carrinho -- pedidos do cliente - -## Mudanças Estruturais Necessárias - -### 1. Resolver host de forma centralizada - -Criar uma camada única para: -- ler `req.hostname` -- classificar o contexto -- extrair `storeSlug` quando aplicável - -Saída esperada: -- `appContext.type = public | partner | store` -- `appContext.storeSlug = string | null` - -### 2. Parar de usar defaults de domínio do Atomenta no PediFoods - -Hoje o PediFoods ainda carrega vários defaults como: -- `https://atomenta.com.br` -- `atomenta_token` -- `atomenta_auth` -- nomes e textos herdados - -Isso precisa ser separado em 2 grupos: - -#### Pode continuar apontando para Atomenta -- integração de API interna -- OTP/crypto -- comunicação backend-to-backend -- consulta de dados mestres, se esta for a arquitetura - -#### Não deve mais apontar para Atomenta -- branding -- domínio público -- base URL de navegação do usuário -- nomes do app -- `package.json` e `ecosystem.config.js` - -### 3. Definir estratégia de sessão/cookies - -Decisão importante: - -O sistema terá **dois contextos de autenticação diferentes**: - -1. **partner session** -- usada no `parceiro.pedifoods.com.br` -- representa o lojista/operador -- dá acesso ao painel administrativo - -2. **customer session** -- usada em `pedifoods.com.br` e em `{slug}.pedifoods.com.br` -- representa o cliente/comprador -- dá acesso à navegação, seleção de lojas, carrinho e compra - -Essas duas sessões **não devem ser compartilhadas entre si**. - -Se fossem compartilhadas: -- um usuário logado como parceiro poderia cair autenticado na storefront da própria loja -- haveria risco de misturar permissões administrativas com experiência de compra -- o modelo de segurança e UX ficaria ambíguo - -#### Opção A -Sessão única compartilhada em `.pedifoods.com.br` - -Resultado: -- mesma sessão valeria para: - - `pedifoods.com.br` - - `parceiro.pedifoods.com.br` - - `{slug}.pedifoods.com.br` - -Problema: -- **não serve para este cenário** -- mistura sessão de lojista com sessão de cliente - -#### Opção B -Sessões separadas por contexto - -Modelo recomendado: -- `parceiro.pedifoods.com.br` - - cookie próprio de parceiro - - escopo exclusivo do painel -- `pedifoods.com.br` - - cookie próprio de cliente - - pode ser compartilhado com `{slug}.pedifoods.com.br` se fizer sentido para a jornada do comprador -- `{slug}.pedifoods.com.br` - - usa sessão do cliente, nunca sessão do parceiro - -Vantagens: -- separa claramente os perfis -- evita vazamento de autenticação entre painel e loja -- reduz risco de autorização indevida -- deixa a jornada de compra independente da jornada administrativa - -Desvantagens: -- exige mais cuidado no desenho de login/logout -- exige nomes de cookies e middleware distintos - -Recomendação: -- usar **sessões separadas por contexto** -- nunca compartilhar sessão de `partner` com `customer` -- se necessário, compartilhar apenas a sessão do **cliente** entre: - - `pedifoods.com.br` - - `{slug}.pedifoods.com.br` - -Implementação sugerida: -- cookie de parceiro: - - exemplo `pedifoods_partner_session` - - domínio restrito a `parceiro.pedifoods.com.br` -- cookie de cliente: - - exemplo `pedifoods_customer_session` - - domínio `.pedifoods.com.br` apenas se a jornada do cliente precisar navegar entre domínio principal e subdomínios de loja - -## Estratégia de Dados - -### Fonte de verdade - -Precisamos assumir explicitamente: -- o Atomenta continua sendo a fonte de verdade operacional -- o PediFoods atua como camada especializada de experiência/lojista/storefront - -Isso significa: -- login e sessão podem existir no PediFoods -- mas dados de loja, catálogo, usuário parceiro e regras sensíveis podem continuar vindo do Atomenta via API - -## Fases de Implementação - -## Fase 1. Higiene e identidade - -Objetivo: -- remover confusão estrutural atual - -Itens: -- corrigir `package.json` do PediFoods -- corrigir `ecosystem.config.js` -- revisar `env.example` -- revisar nomes, textos e defaults públicos -- separar claramente o que é branding PediFoods e o que é integração Atomenta - -Saída: -- o projeto deixa de se identificar como Atomenta - -## Fase 2. Resolver host e contexto - -Objetivo: -- introduzir leitura de subdomínio - -Itens: -- criar utilitário central de hostname -- classificar `public`, `partner`, `store` -- disponibilizar contexto em middleware -- ajustar templates/layouts e rotas para reagirem ao contexto - -Saída: -- uma única app responde diferente por host - -## Fase 3. Fluxo público `pedifoods.com.br` - -Objetivo: -- consolidar homepage + login + listagem de lojas - -Itens: -- manter home atual -- após login, mostrar lojas do usuário -- ao clicar na loja, redirecionar para `slug.pedifoods.com.br` - -Saída: -- domínio principal vira hub do usuário - -## Fase 4. Fluxo parceiro `parceiro.pedifoods.com.br` - -Objetivo: -- isolar o backoffice do lojista - -Itens: -- mover/ajustar as telas que hoje se comportam como "Atomenta Store" -- garantir que esse host sempre abra painel parceiro -- revisar permissões e middleware - -Saída: -- painel do lojista com identidade própria - -## Fase 5. Fluxo loja `slug.pedifoods.com.br` - -Objetivo: -- transformar storefront em domínio por loja - -Itens: -- mapear slug -> loja -- carregar tema, catálogo e dados por slug -- revisar SEO/meta tags -- revisar links absolutos - -Saída: -- cada loja com seu subdomínio - -## Fase 6. Sessão compartilhada - -Objetivo: -- permitir navegação fluida entre subdomínios - -Itens: -- cookie domain `.pedifoods.com.br` -- revisar login/logout -- revisar redirecionamentos -- revisar callbacks OTP - -Saída: -- usuário loga uma vez e navega entre contextos compatíveis - -## Riscos - -### 1. Misturar branding com integração - -Se trocar tudo de `atomenta` cegamente, pode quebrar integrações reais. - -### 2. Cookies atuais - -Se hoje os cookies ainda usam nomes herdados, uma troca brusca pode invalidar sessões e OTP. - -### 3. Links absolutos hardcoded - -Há muitos pontos do PediFoods ainda apontando para Atomenta. -Eles precisam ser classificados antes de alterar. - -### 4. Deploy atual - -Hoje o deploy sobe um projeto que ainda está semanticamente misturado. -Sem limpeza mínima, a arquitetura nova vai nascer confusa. - -## Recomendações Práticas - -### Curto prazo - -- corrigir identidade mínima do projeto PediFoods -- mapear todos os usos de: - - `atomenta.com.br` - - `atomenta_token` - - `atomenta_auth` - - `ATOMENTA_*` -- classificar cada ocorrência em: - - branding - - integração legítima - -### Médio prazo - -- implementar middleware de contexto por host -- separar layouts `public`, `partner`, `store` -- começar pelo domínio `parceiro.pedifoods.com.br` - -### Longo prazo - -- consolidar storefront por slug -- revisar autenticação multi-subdomínio -- avaliar se no futuro compensa separar em múltiplos apps - -## Ordem Recomendada de Execução - -1. Higienizar identidade do projeto PediFoods -2. Mapear referências herdadas de Atomenta -3. Criar resolvedor central de host/subdomínio -4. Subir `parceiro.pedifoods.com.br` -5. Ajustar `pedifoods.com.br` para login + lista de lojas -6. Implementar `slug.pedifoods.com.br` -7. Revisar cookies e sessão compartilhada - -## Decisão Recomendada Agora - -A melhor decisão agora é: - -- **não tentar “desatomentar” tudo de uma vez** -- primeiro separar: - - identidade pública - - painel parceiro - - storefront por subdomínio -- e só depois revisar dependências herdadas uma a uma - -## Próximo Passo - -Próxima entrega recomendada: - -1. fazer uma auditoria do repositório PediFoods -2. classificar referências herdadas em: - - `pode trocar agora` - - `depende de integração com Atomenta` -3. implementar o resolvedor de subdomínio -4. subir primeiro o contexto `parceiro.pedifoods.com.br` - -## Tasks de Implementação - -Legenda: -- 🔴 pendente -- 🟢 concluído - -### Fase 1. Higiene do projeto PediFoods - -- 🟢 Corrigir identidade básica do projeto em `package.json` -- 🟢 Corrigir identidade básica do processo em `ecosystem.config.js` -- 🔴 Revisar `env.example` para separar branding PediFoods de integração Atomenta -- 🟢 Mapear todas as referências a `atomenta` no repositório -- 🟢 Classificar cada referência como: - - branding/UI - - integração técnica legítima - - legado morto -- 🟢 Remover ou ajustar branding incorreto do PediFoods - -### Fase 2. Contexto de domínio e subdomínio - -- 🟢 Criar utilitário central para resolver o host atual -- 🟢 Criar classificador de contexto: - - `public` - - `partner` - - `store` -- 🟢 Extrair `storeSlug` automaticamente quando o host for `{slug}.pedifoods.com.br` -- 🟢 Injetar esse contexto em middleware global -- 🟢 Expor o contexto para views/templates -- 🔴 Garantir fallback seguro para domínio desconhecido - -### Fase 3. Sessões separadas - -- 🟢 Criar sessão de parceiro independente -- 🔴 Criar sessão de cliente independente -- 🔴 Garantir que `parceiro.pedifoods.com.br` nunca reutilize sessão de cliente -- 🔴 Garantir que `{slug}.pedifoods.com.br` nunca reutilize sessão de parceiro -- 🟢 Revisar nomes dos cookies -- 🟢 Revisar `login`, `logout` e middleware de autenticação -- 🟢 Revisar redirecionamentos pós-login por contexto - -### Fase 4. `pedifoods.com.br` - -- 🟢 Consolidar a home pública -- 🟢 Consolidar login/cadastro/recuperação -- 🔴 Criar tela pós-login com listagem de lojas do usuário -- 🟢 Fazer o clique na loja redirecionar para `{slug}.pedifoods.com.br` -- 🟢 Revisar meta tags, canonical e links absolutos - -### Fase 5. `parceiro.pedifoods.com.br` - -- 🟢 Isolar o painel do lojista nesse host -- 🟢 Revisar middleware e permissões do painel -- 🟢 Revisar links internos do painel para não mandar ao domínio errado -- 🟢 Ajustar onboarding do parceiro -- 🟢 Ajustar rotas administrativas de loja, pedidos, catálogo e financeiro -- 🟢 Garantir que o painel não renderize a experiência pública da loja - -### Fase 6. `{slug}.pedifoods.com.br` - -- 🟢 Criar resolução da loja por slug -- 🟢 Buscar loja e catálogo pelo slug -- 🟢 Renderizar storefront por loja -- 🟢 Ajustar tema, título, descrição e identidade da loja -- 🟢 Ajustar links compartilháveis da loja -- 🟢 Revisar SEO e OpenGraph por loja -- 🔴 Garantir fallback para slug inexistente - -### Fase 7. Gestão de slug da loja - -- 🟢 Adicionar campo `storeSlug` como atributo persistido da loja -- 🟢 Validar unicidade global do slug -- 🟢 Validar formato do slug -- 🟢 Gerar pré-preenchimento automático do slug no fim do onboarding com base no nome fantasia da loja -- 🟢 Criar tela de configuração do slug no painel parceiro -- 🟢 Exibir aviso de impacto da troca de slug -- 🟢 Salvar data da última alteração de slug -- 🟢 Bloquear nova alteração por 90 dias -- 🟢 Exibir contador ou data de desbloqueio para nova alteração -- 🔴 Criar redirect temporário do slug antigo para o novo, se viável - -### Fase 8. Integração com Atomenta - -- 🟢 Revisar quais chamadas devem continuar indo ao Atomenta -- 🟢 Centralizar base URL interna da API do Atomenta -- 🔴 Revisar OTP e autenticação compartilhada -- 🟢 Revisar cookies herdados `atomenta_*` -- 🔴 Revisar endpoints de catálogo, loja, financeiro e customer lookup -- 🔴 Remover dependências herdadas desnecessárias - -### Fase 9. Deploy e infraestrutura - -- 🔴 Ajustar nginx/proxy para: - - `pedifoods.com.br` - - `parceiro.pedifoods.com.br` - - `*.pedifoods.com.br` -- 🔴 Validar wildcard localmente antes da etapa de DNS público -- 🔴 Garantir certificado TLS compatível com wildcard -- 🔴 Validar resolução correta de host até a app Node - -## Status Atual - -Legenda: -- 🟢 concluído -- 🟡 parcial -- 🔴 pendente - -Resumo do que já foi entregue no app: -- 🟢 identidade básica do projeto corrigida -- 🟢 resolução central de host/subdomínio -- 🟢 separação inicial entre `public`, `partner` e `store` -- 🟢 slug canônico com preview, unicidade, cooldown de 90 dias e geração automática no onboarding -- 🟢 storefront canônica em `{slug}.pedifoods.com.br` -- 🟢 links públicos e links do painel abrindo a loja publicada correta -- 🟢 limpeza principal de branding visível do PediFoods -- 🟢 frontend priorizando `pedifoods_partner_token` com fallback para legado - -O que ainda falta para fechar o fluxo completo: -- 🔴 autenticação real do cliente -- 🔴 tela pós-login do cliente com listagem de lojas -- 🔴 fallback para slug inexistente -- 🔴 redirect temporário de slug antigo para slug novo -- 🔴 wildcard real em proxy/Cloudflare/TLS -- 🔴 Validar deploy do PediFoods sem contaminar com Atomenta -- 🔴 Configurar wildcard DNS no Cloudflare - - esta task deve ficar por último - - quando chegarmos nela, você executa manualmente no Cloudflare - - eu só vou te avisar e te passar o valor exato - -### Fase 10. Testes de ponta a ponta - -- 🔴 Testar login de cliente em `pedifoods.com.br` -- 🔴 Testar seleção de loja e redirecionamento para `{slug}.pedifoods.com.br` -- 🔴 Testar login de parceiro em `parceiro.pedifoods.com.br` -- 🔴 Testar isolamento entre sessão de parceiro e sessão de cliente -- 🔴 Testar troca de slug com bloqueio de 90 dias -- 🔴 Testar slug inválido/inexistente -- 🔴 Testar logout em cada contexto -- 🔴 Testar links absolutos, redirects e cookies - -## Política de Slug da Loja - -Sua ideia funciona e faz sentido. - -### Recomendação - -O slug deve ser: -- criado uma primeira vez no onboarding da loja -- editável pelo parceiro no painel -- bloqueado por 90 dias após cada alteração - -### Motivos para essa regra - -- evita troca frequente de URL -- reduz confusão para clientes recorrentes -- protege links já compartilhados -- reduz impacto operacional em cache, indexing e hábito do usuário - -### Como funcionaria - -Cada loja teria campos como: -- `storeSlug` -- `slugUpdatedAt` -- `slugLockedUntil` -- `slugGeneratedBySystemAt` -- `slugFirstManualUpdateAt` - -Regra: -- se nunca definiu slug: - - o sistema pré-preenche automaticamente - - o usuário pode ajustar antes de concluir -- se já definiu: - - só pode alterar novamente após 90 dias - -### Geração inicial do slug - -Recomendação: -- ao terminar o onboarding, gerar um pré-preenchimento automático baseado no nome fantasia - -Exemplo: -- `Pizzaria do Zé` -> `pizzaria-do-ze` -- `Temaki House Prime` -> `temaki-house-prime` - -O usuário: -- vê o slug sugerido -- pode editar antes de salvar pela primeira vez - -Importante: -- o slug gerado automaticamente pelo sistema **não inicia** a janela de bloqueio de 90 dias -- ele serve apenas como valor inicial/sugestão persistida -- o contador de 90 dias só começa quando houver a **primeira alteração manual do lojista** - -### Limite de tamanho - -Recomendação: -- limitar o slug a **30 caracteres** - -Esse limite deve contar: -- letras -- números -- hífens - -Na prática: -- espaços viram hífen -- então o total final do slug salvo não pode passar de `30` - -Exemplo de regra: -- nome fantasia muito grande: - - `Loja Muito Grande Com Nome Enorme da Silva` -- slug gerado: - - `loja-muito-grande-com-nome` - -### Regra de sanitização sugerida - -O gerador automático deve: -- converter para minúsculas -- remover acentos -- trocar espaços por hífen -- remover caracteres especiais -- colapsar hífens duplicados -- cortar em `30` caracteres -- remover hífen do início/fim - -### Se o slug já existir - -Se o slug gerado já estiver em uso: -- adicionar sufixo incremental curto - -Exemplo: -- `pizzaria-do-ze` -- `pizzaria-do-ze-2` -- `pizzaria-do-ze-3` - -Sempre respeitando o limite final de `30` caracteres - -### Regra de bloqueio de 90 dias - -Regra correta: - -- **geração automática pelo sistema** - - não conta como alteração manual - - não bloqueia o campo - -- **primeira edição manual feita pelo lojista** - - passa a contar como alteração real - - grava `slugFirstManualUpdateAt` - - grava `slugUpdatedAt` - - define `slugLockedUntil = slugUpdatedAt + 90 dias` - -- **edições seguintes** - - só permitidas após `slugLockedUntil` - -### Exemplo prático - -1. onboarding termina -2. sistema gera: - - `pizzaria-do-ze` -3. lojista vê o valor e troca para: - - `pizza-do-ze-centro` -4. essa troca manual: - - conta como primeira alteração real - - inicia a trava de 90 dias - -Se o lojista aceitar o slug automático sem mexer: -- nenhum bloqueio de 90 dias deve ser iniciado -- ele ainda pode fazer a primeira alteração manual depois - -### Ajuste das tasks - -- 🔴 Persistir metadado informando se o slug atual foi gerado automaticamente ou definido manualmente -- 🔴 Garantir que a geração automática no onboarding não inicie `slugLockedUntil` -- 🔴 Iniciar a janela de 90 dias somente na primeira alteração manual do lojista - -### Validações necessárias - -- slug único no sistema -- somente letras minúsculas, números e hífen -- tamanho mínimo e máximo -- blacklist de reservados, por exemplo: - - `www` - - `api` - - `admin` - - `app` - - `parceiro` - - `mail` - - `git` - - `proxy` - -### Redirect do slug antigo - -Se possível, é recomendável: -- guardar o slug antigo -- fazer redirect `301` do slug antigo para o novo por um período - -Isso ajuda: -- SEO -- links salvos -- usuários acostumados com a URL antiga - -### Sobre DNS e Cloudflare - -Isso funciona bem se você usar: -- wildcard DNS `*.pedifoods.com.br` -- proxy reverso lendo o `Host` - -Nesse modelo: -- você **não precisa criar um registro DNS novo por loja** -- o wildcard cobre todos os slugs -- a troca de slug depende mais da aplicação e do cache HTTP do que de propagação DNS individual - -Ou seja: -- a trava de 90 dias continua fazendo sentido por produto e UX -- mas não por limitação técnica de DNS em si - -### Recomendação final para slug - -Implementar assim: - -- slug definido no onboarding -- edição permitida no painel parceiro -- nova alteração somente após 90 dias -- aviso visual claro antes de salvar -- registro do slug antigo para redirect temporário - -Autor: Daniel Arantes Loverde