Skip and docs

This commit is contained in:
Daniel Arantes Loverde
2026-02-04 08:42:32 -03:00
commit 78daaf1927
114 changed files with 3326 additions and 0 deletions

358
API_Mobile_App.md Normal file
View File

@@ -0,0 +1,358 @@
# 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 <Users_JWT_Token>`
- **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 <Users_JWT_Token>` (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:**
```json
{
"email": "daniel@example.com",
"otp": "123456" // Em dev/demo, apenas email pode bastar se configurado assim
}
```
**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 <JWT_Token>`
- `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": [],
"behavioral_stats": {
"total_orders": 5,
"avg_ticket_size": 45.00
}
}
}
```
### 4. Update Profile
**POST** `/api/customer/:id`
**Headers:**
- `Authorization: Bearer <JWT_Token>`
- `Atomenta-Token: 550e8400-e29b-41d4-a716-44665544000a`
**Body:**
```json
{
"name": "Daniel A. Loverde", // Fields to update
"phoneNumber": "+5511988888888",
"biometricsEnabled": true
}
```
---
## 🏠 App Home Screen
### List Stores (Smart Listing)
**GET** `/api/app/stores`
**Headers:**
- `Authorization: Bearer <Users_JWT_Token>` (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 <JWT>`
**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`
Returns detailed info including operating hours and accepted payment methods.
**Response:**
```json
{
"error": false,
"code": "STORE_INFO_RETRIEVED",
"result": {
"isOpen": true,
"paymentMethods": {
"paymentOnDelivery": true,
"paymentOnPickup": true,
"acceptCash": true,
"acceptPix": true,
"acceptCreditCard": true, // Resumo Geral
"acceptDebitCard": true, // Resumo Geral
"brands": {
"credit": {
"visa": true,
"master": true,
"elo": true,
"amex": false,
"hipercard": false
},
"debit": {
"visa": true,
"master": true,
"elo": true
}
}
},
"openingHours": { ... },
"address": { ... }
}
}
```
### 3. Product Catalog
**GET** `/api/store/:storeId/catalog`
Returns categories with their respective products.
**Response:**
```json
{
"error": false,
"result": [
{
"id": "cat_1...",
"name": "Burgers",
"products": [
{
"id": "prod_1...",
"name": "Big Mac",
"price": 25.90,
"image": "https://...",
"addonGroups": [...]
}
]
}
]
}
```
### 4. 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",
"qty": 2,
"price": 25.00,
"addons": []
}
],
"total": 50.00,
"paymentMethod": "PIX", // "PIX" | "CREDIT_CARD" | "DEBIT_CARD" | "MONEY"
"deliveryType": "DELIVERY", // "DELIVERY" | "PICKUP"
"address": {
"street": "Rua Exemplo",
"number": "123",
"neighborhood": "Centro"
}
}
```
**Response:**
```json
{
"error": false,
"code": "ORDER_CREATED",
"result": {
"id": "ord_987...",
"status": "created",
"paymentPayload": "https://www.asaas.com/i/..." // Link para pagamento (Pix/Boleto)
}
}
```

389
API_Store_External.md Normal file
View File

@@ -0,0 +1,389 @@
# 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 <JWT_TOKEN>`
---
## 🏬 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)
### 4. Categorias de 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 } ]
}
}
```
### 5. 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
### 6. 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.
```
### 7. Detalhes de um Pedido Individual
**GET** `/api/store/:storeId/orders/:orderId`
Retorna o objeto completo do pedido (parity 100% com o painel).
---
## 💰 Financeiro Detalhado
### 7. 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
### 8. 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. 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": "..."
}
]
}
```
**Resposta Sucesso:**
```json
{
"error": false,
"result": {
"id": "order_guid",
"shortId": "1234",
"status": "PENDING",
"paymentStatus": "PENDING",
"paymentPayload": "..." // Se PIX (Big String Base64 ou Copia e Cola)
}
}
```

76
pedi-foods/.gitignore vendored Normal file
View File

@@ -0,0 +1,76 @@
## User settings
# vi
.*.swp
.*.swo
# macOS
.DS_Store
# gradle properties
local.properties
.gradle/
.android/
.kotlin/
Android/app/keystore.jks
Android/app/keystore.properties
xcodebuild*.log
default.profraw
*.mobileprovision
*.cer
*.p12
*.p12.password
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
.swiftpm
.build/
build/
DerivedData/
xcuserdata/
xcodebuild*.log
.idea/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
*.xcscmblueprint
*.xccheckout
## Obj-C/Swift specific
*.hmap
## App packaging
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
Packages/
Package.pins
Package.resolved
#*.xcodeproj
Carthage/Build/
# fastlane
**/fastlane/apikey.json
**/fastlane/report.xml
**/fastlane/README.md
**/fastlane/Preview.html
**/fastlane/test_output

View File

@@ -0,0 +1,91 @@
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
}
buildFeatures {
buildConfig = true
}
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")
}
}
}

View File

@@ -0,0 +1,10 @@
-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.** { *; }

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This AndroidManifest.xml template was generated by Skip -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<!-- example permissions for using device location -->
<!-- <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> -->
<!-- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> -->
<!-- permissions needed for using the internet or an embedded WebKit browser -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
<application
android:label="${PRODUCT_NAME}"
android:name=".AndroidAppMain"
android:supportsRtl="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode"
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,167 @@
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 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
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 {
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()
// Example of requesting permissions on startup.
// These must match the permissions in the AndroidManifest.xml file.
//let permissions = listOf(
// Manifest.permission.ACCESS_COARSE_LOCATION,
// Manifest.permission.ACCESS_FINE_LOCATION
// Manifest.permission.CAMERA,
// Manifest.permission.WRITE_EXTERNAL_STORAGE,
//)
//let requestTag = 1
//ActivityCompat.requestPermissions(self, permissions.toTypedArray(), requestTag)
}
override fun onStart() {
logger.info("onStart")
super.onStart()
}
override fun onResume() {
super.onResume()
AppDelegate.shared.onResume()
}
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<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
logger.info("onRequestPermissionsResult: ${requestCode}")
}
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)
}
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,11 @@
# 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")

View File

@@ -0,0 +1,51 @@
# 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

View File

@@ -0,0 +1 @@
A great new app built with Skip!

View File

@@ -0,0 +1 @@
A great new app built with Skip!

View File

@@ -0,0 +1 @@
PediFoods

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4g
android.useAndroidX=true
kotlin.code.style=official

View File

@@ -0,0 +1 @@
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip

View File

@@ -0,0 +1,25 @@
// 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
}

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -0,0 +1,122 @@
{
"images" : [
{
"filename" : "AppIcon-20@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "AppIcon-20@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "AppIcon-29.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "AppIcon-29@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "AppIcon-29@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "AppIcon-40@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "AppIcon-40@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "AppIcon@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "AppIcon@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "AppIcon-20~ipad.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "AppIcon-20@2x~ipad.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "AppIcon-29~ipad.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "AppIcon-29@2x~ipad.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "AppIcon-40~ipad.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "AppIcon-40@2x~ipad.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "AppIcon~ipad.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "AppIcon@2x~ipad.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "AppIcon-83.5@2x~ipad.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "AppIcon~ios-marketing.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,58 @@
#include "../Skip.env"
// Set the action that will be executed as part of the Xcode Run Script phase
// Setting to "launch" will build and run the app in the first open Android emulator or device
// Setting to "build" will just run gradle build, but will not launch the app
// Setting to "none" will completely disable the build and launch of the Android app
SKIP_ACTION = launch
//SKIP_ACTION = build
//SKIP_ACTION = none
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor
INFOPLIST_FILE = Info.plist
GENERATE_INFOPLIST_FILE = YES
// The user-visible name of the app (localizable)
//INFOPLIST_KEY_CFBundleDisplayName = App Name
//INFOPLIST_KEY_LSApplicationCategoryType = public.app-category.utilities
//INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "This app uses your location to …"
// iOS-specific Info.plist property keys
INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphone*] = YES
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphone*] = YES
INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphone*] = YES
INFOPLIST_KEY_UIStatusBarStyle[sdk=iphone*] = UIStatusBarStyleDefault
INFOPLIST_KEY_UISupportedInterfaceOrientations[sdk=iphone*] = UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown
IPHONEOS_DEPLOYMENT_TARGET = 17.0
MACOSX_DEPLOYMENT_TARGET = 14.0
SUPPORTS_MACCATALYST = NO
// iPhone + iPad
TARGETED_DEVICE_FAMILY = 1,2
// iPhone only
// TARGETED_DEVICE_FAMILY = 1
SWIFT_EMIT_LOC_STRINGS = YES
// the name of the product module; this can be anything, but cannot conflict with any Swift module names
PRODUCT_MODULE_NAME = $(PRODUCT_NAME:c99extidentifier)App
// On-device testing may need to override the bundle ID
// PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=iphoneos*] = cool.beans.BundleIdentifer
SDKROOT = auto
SUPPORTED_PLATFORMS = iphoneos iphonesimulator macosx
SWIFT_EMIT_LOC_STRINGS = YES
SWIFT_VERSION = 6
// Development team ID for on-device testing
CODE_SIGNING_REQUIRED = NO
CODE_SIGN_STYLE = Automatic
CODE_SIGN_ENTITLEMENTS = Entitlements.plist
//CODE_SIGNING_IDENTITY = -
//DEVELOPMENT_TEAM =

View File

@@ -0,0 +1,289 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objects = {
/* Begin PBXBuildFile section */
491F27822DA55B72004926EE /* PediFoods in Frameworks */ = {isa = PBXBuildFile; productRef = 491F27812DA55B72004926EE /* PediFoods */; };
491F27832DA55B72004926EE /* PediFoods in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 491F27812DA55B72004926EE /* PediFoods */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
496BDBEE2B8A7E9C00C09264 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */; };
499CD43B2AC5B799001AE8D8 /* Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F90C2B2A52156200F06D93 /* Main.swift */; };
499CD4402AC5B799001AE8D8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 49F90C2F2A52156300F06D93 /* Assets.xcassets */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
499CD44A2AC5B9C6001AE8D8 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
491F27832DA55B72004926EE /* PediFoods in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
4900101C2BACEA710000DE33 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
493609562A6B7EAE00C401E2 /* PediFoods */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = PediFoods; path = ..; sourceTree = "<group>"; };
496BDBEB2B89A47800C09264 /* PediFoods.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PediFoods.app; sourceTree = BUILT_PRODUCTS_DIR; };
496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = ../Sources/PediFoods/Resources/Localizable.xcstrings; sourceTree = "<group>"; };
496EB72F2A6AE4DE00C1253A /* Skip.env */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Skip.env; path = ../Skip.env; sourceTree = "<group>"; };
496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = PediFoods.xcconfig; sourceTree = "<group>"; };
496EB72F2A6AE4DE00C1253C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
499AB9082B0581F4005E8330 /* plugins */ = {isa = PBXFileReference; lastKnownFileType = folder; name = plugins; path = ../../Intermediates.noindex/BuildToolPluginIntermediates; sourceTree = BUILT_PRODUCTS_DIR; };
49F90C2B2A52156200F06D93 /* Main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Main.swift; path = Sources/Main.swift; sourceTree = SOURCE_ROOT; };
49F90C2F2A52156300F06D93 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
49F90C312A52156300F06D93 /* Entitlements.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Entitlements.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
499CD43C2AC5B799001AE8D8 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
491F27822DA55B72004926EE /* PediFoods in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
496BDBEC2B89A47800C09264 /* Products */ = {
isa = PBXGroup;
children = (
496BDBEB2B89A47800C09264 /* PediFoods.app */,
);
name = Products;
sourceTree = "<group>";
};
49AB54462B066A7E007B79B2 /* SkipStone */ = {
isa = PBXGroup;
children = (
499AB9082B0581F4005E8330 /* plugins */,
);
name = SkipStone;
sourceTree = "<group>";
};
49F90C1F2A52156200F06D93 = {
isa = PBXGroup;
children = (
496EB72F2A6AE4DE00C1253C /* README.md */,
496EB72F2A6AE4DE00C1253A /* Skip.env */,
496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */,
496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */,
493609562A6B7EAE00C401E2 /* PediFoods */,
49F90C2A2A52156200F06D93 /* App */,
49AB54462B066A7E007B79B2 /* SkipStone */,
496BDBEC2B89A47800C09264 /* Products */,
);
sourceTree = "<group>";
};
49F90C2A2A52156200F06D93 /* App */ = {
isa = PBXGroup;
children = (
49F90C2B2A52156200F06D93 /* Main.swift */,
49F90C2F2A52156300F06D93 /* Assets.xcassets */,
49F90C312A52156300F06D93 /* Entitlements.plist */,
4900101C2BACEA710000DE33 /* Info.plist */,
);
name = App;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
499CD4382AC5B799001AE8D8 /* PediFoods App */ = {
isa = PBXNativeTarget;
buildConfigurationList = 499CD4412AC5B799001AE8D8 /* Build configuration list for PBXNativeTarget "PediFoods App" */;
buildPhases = (
499CD43A2AC5B799001AE8D8 /* Sources */,
499CD43C2AC5B799001AE8D8 /* Frameworks */,
499CD43E2AC5B799001AE8D8 /* Resources */,
499CD4452AC5B869001AE8D8 /* Run skip gradle */,
499CD44A2AC5B9C6001AE8D8 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = "PediFoods App";
packageProductDependencies = (
491F27812DA55B72004926EE /* PediFoods */,
);
productName = App;
productReference = 496BDBEB2B89A47800C09264 /* PediFoods.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
49F90C202A52156200F06D93 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1430;
LastUpgradeCheck = 1630;
};
buildConfigurationList = 49F90C232A52156200F06D93 /* Build configuration list for PBXProject "PediFoods" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
es,
ja,
"zh-Hans",
);
mainGroup = 49F90C1F2A52156200F06D93;
packageReferences = (
);
productRefGroup = 496BDBEC2B89A47800C09264 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
499CD4382AC5B799001AE8D8 /* PediFoods App */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
499CD43E2AC5B799001AE8D8 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
499CD4402AC5B799001AE8D8 /* Assets.xcassets in Resources */,
496BDBEE2B8A7E9C00C09264 /* Localizable.xcstrings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
499CD4452AC5B869001AE8D8 /* Run skip gradle */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Run skip gradle";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = "/bin/sh -e";
shellScript = "if [ \"${SKIP_ZERO}\" != \"\" ]; then\n echo \"note: skipping skip due to SKIP_ZERO\"\n exit 0\nelif [ \"${ENABLE_PREVIEWS}\" = \"YES\" ]; then\n echo \"note: skipping skip due to ENABLE_PREVIEWS\"\n exit 0\nelif [ \"${ACTION}\" = \"install\" ]; then\n echo \"note: skipping skip due to archive install\"\n exit 0\nelif [ \"${SKIP_ACTION}\" = \"none\" ]; then\n echo \"note: skipping skip due to SKIP_ACTION none\"\n exit 0\nelse\n SKIP_ACTION=\"${SKIP_ACTION:-launch}\"\nfi\nPATH=${BUILD_ROOT}/Release:${BUILD_ROOT}/Debug:${BUILD_ROOT}/../../SourcePackages/artifacts/skip/skip/skip.artifactbundle/macos:${PATH}:${HOMEBREW_PREFIX:-/opt/homebrew}/bin\necho \"note: running gradle build with: $(which skip) gradle -p ${PWD}/../Android ${SKIP_ACTION:-launch}${CONFIGURATION:-Debug}\"\nskip gradle -p ../Android ${SKIP_ACTION:-launch}${CONFIGURATION:-Debug}\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
499CD43A2AC5B799001AE8D8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
499CD43B2AC5B799001AE8D8 /* Main.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
499CD4422AC5B799001AE8D8 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */;
buildSettings = {
ENABLE_PREVIEWS = YES;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
};
name = Debug;
};
499CD4432AC5B799001AE8D8 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */;
buildSettings = {
ENABLE_PREVIEWS = YES;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
};
name = Release;
};
49F90C4B2A52156300F06D93 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
49F90C4C2A52156300F06D93 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
499CD4412AC5B799001AE8D8 /* Build configuration list for PBXNativeTarget "PediFoods App" */ = {
isa = XCConfigurationList;
buildConfigurations = (
499CD4422AC5B799001AE8D8 /* Debug */,
499CD4432AC5B799001AE8D8 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
49F90C232A52156200F06D93 /* Build configuration list for PBXProject "PediFoods" */ = {
isa = XCConfigurationList;
buildConfigurations = (
49F90C4B2A52156300F06D93 /* Debug */,
49F90C4C2A52156300F06D93 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */
491F27812DA55B72004926EE /* PediFoods */ = {
isa = XCSwiftPackageProductDependency;
productName = PediFoods;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 49F90C202A52156200F06D93 /* Project object */;
}

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1630"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "499CD4382AC5B799001AE8D8"
BuildableName = "PediFoods.app"
BlueprintName = "PediFoods App"
ReferencedContainer = "container:PediFoods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "499CD4382AC5B799001AE8D8"
BuildableName = "PediFoods.app"
BlueprintName = "PediFoods App"
ReferencedContainer = "container:PediFoods.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "499CD4382AC5B799001AE8D8"
BuildableName = "PediFoods.app"
BlueprintName = "PediFoods App"
ReferencedContainer = "container:PediFoods.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,86 @@
import SwiftUI
import PediFoods
private typealias AppRootView = PediFoodsRootView
private typealias AppDelegate = PediFoodsAppDelegate
/// The entry point to the app simply loads the App implementation from SPM module.
@main struct AppMain: App {
@AppDelegateAdaptor(AppMainDelegate.self) var appDelegate
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
AppRootView()
}
.onChange(of: scenePhase) { oldPhase, newPhase in
switch newPhase {
case .active:
AppDelegate.shared.onResume()
case .inactive:
AppDelegate.shared.onPause()
case .background:
AppDelegate.shared.onStop()
@unknown default:
print("unknown app phase: \(newPhase)")
}
}
}
}
#if canImport(UIKit)
typealias AppDelegateAdaptor = UIApplicationDelegateAdaptor
typealias AppMainDelegateBase = UIApplicationDelegate
typealias AppType = UIApplication
#elseif canImport(AppKit)
typealias AppDelegateAdaptor = NSApplicationDelegateAdaptor
typealias AppMainDelegateBase = NSApplicationDelegate
typealias AppType = NSApplication
#endif
@MainActor final class AppMainDelegate: NSObject, AppMainDelegateBase {
let application = AppType.shared
#if canImport(UIKit)
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
AppDelegate.shared.onInit()
return true
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
AppDelegate.shared.onLaunch()
return true
}
func applicationWillTerminate(_ application: UIApplication) {
AppDelegate.shared.onDestroy()
}
func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
AppDelegate.shared.onLowMemory()
}
// support for SkipNotify.fetchNotificationToken()
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
NotificationCenter.default.post(name: NSNotification.Name("didRegisterForRemoteNotificationsWithDeviceToken"), object: application, userInfo: ["deviceToken": deviceToken])
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error) {
NotificationCenter.default.post(name: NSNotification.Name("didFailToRegisterForRemoteNotificationsWithError"), object: application, userInfo: ["error": error])
}
#elseif canImport(AppKit)
func applicationWillFinishLaunching(_ notification: Notification) {
AppDelegate.shared.onInit()
}
func applicationDidFinishLaunching(_ notification: Notification) {
AppDelegate.shared.onLaunch()
}
func applicationWillTerminate(_ application: Notification) {
AppDelegate.shared.onDestroy()
}
#endif
}

View File

@@ -0,0 +1,5 @@
// Additional properties included by the Fastfile build_app
// This file can be used to override various properties from Skip.env
//PRODUCT_BUNDLE_IDENTIFIER =
//DEVELOPMENT_TEAM =

View File

@@ -0,0 +1,8 @@
# For more information about the Appfile, see:
# https://docs.fastlane.tools/advanced/#appfile
require('dotenv')
Dotenv.load '../../Skip.env'
app_identifier(ENV['ANDROID_APPLICATION_ID'] || ENV['PRODUCT_BUNDLE_IDENTIFIER'])
# apple_id("my@email")

View File

@@ -0,0 +1,27 @@
copyright "#{Time.now.year}"
force(true) # Skip HTML report verification
automatic_release(true)
skip_screenshots(false)
precheck_include_in_app_purchases(false)
#skip_binary_upload(true)
submit_for_review(true)
submission_information({
add_id_info_serves_ads: false,
add_id_info_uses_idfa: false,
add_id_info_tracks_install: false,
add_id_info_tracks_action: false,
add_id_info_limits_tracking: false,
content_rights_has_rights: false,
content_rights_contains_third_party_content: false,
export_compliance_contains_third_party_cryptography: false,
export_compliance_encryption_updated: false,
export_compliance_platform: 'ios',
export_compliance_compliance_required: false,
export_compliance_uses_encryption: false,
export_compliance_is_exempt: false,
export_compliance_contains_proprietary_cryptography: false
})

View File

@@ -0,0 +1,40 @@
# This file contains the fastlane.tools configuration
# for the iOS half of the Skip app.
# You can find the documentation at https://docs.fastlane.tools
default_platform(:ios)
lane :assemble do |options|
# only build the iOS side of the app
ENV["SKIP_ZERO"] = "true"
build_app(
scheme: "PediFoods App",
sdk: "iphoneos",
xcconfig: "fastlane/AppStore.xcconfig",
xcargs: "-skipPackagePluginValidation -skipMacroValidation",
derived_data_path: "../.build/Darwin/DerivedData",
output_directory: "../.build/fastlane/Darwin",
skip_archive: ENV["FASTLANE_SKIP_ARCHIVE"] == "YES",
skip_codesigning: ENV["FASTLANE_SKIP_CODESIGNING"] == "YES"
)
end
lane :release do |options|
desc "Build and release app"
# see https://docs.fastlane.tools/uploading-app-privacy-details/
#upload_app_privacy_details_to_app_store(json_path: "fastlane/app_privacy_details.json")
# if you have an apikey.json file (https://developer.apple.com/documentation/appstoreconnectapi/creating-api-keys-for-app-store-connect-api), fastlane can automatically fetch certificates and the ASC authentication information
#get_certificates(api_key_path: "fastlane/apikey.json")
get_provisioning_profile(api_key_path: "fastlane/apikey.json")
assemble
upload_to_app_store(
api_key_path: "fastlane/apikey.json",
app_rating_config_path: "fastlane/metadata/rating.json",
release_notes: { default: "Fixes and improvements." }
)
end

View File

@@ -0,0 +1,7 @@
[
{
"data_protections": [
"DATA_NOT_COLLECTED"
]
}
]

View File

@@ -0,0 +1 @@
A great new app built with Skip!

View File

@@ -0,0 +1 @@
app,key,words

View File

@@ -0,0 +1 @@
https://example.org/privacy/

View File

@@ -0,0 +1 @@
Bug fixes and performance improvements.

View File

@@ -0,0 +1 @@
https://example.org/app/

View File

@@ -0,0 +1 @@
A new Skip app

View File

@@ -0,0 +1 @@
https://example.org/support/

View File

@@ -0,0 +1 @@
PediFoods

View File

@@ -0,0 +1 @@
New features and better performance.

View File

@@ -0,0 +1,18 @@
{
"alcoholTobaccoOrDrugUseOrReferences": "NONE",
"contests": "NONE",
"gamblingSimulated": "NONE",
"horrorOrFearThemes": "NONE",
"koreaAgeRatingOverride": "NONE",
"matureOrSuggestiveThemes": "NONE",
"medicalOrTreatmentInformation": "NONE",
"profanityOrCrudeHumor": "NONE",
"sexualContentGraphicAndNudity": "NONE",
"sexualContentOrNudity": "NONE",
"violenceCartoonOrFantasy": "NONE",
"violenceRealisticProlongedGraphicOrSadistic": "NONE",
"violenceRealistic": "NONE",
"gambling": false,
"lootBox": false,
"unrestrictedWebAccess": false
}

21
pedi-foods/Package.swift Normal file
View File

@@ -0,0 +1,21 @@
// swift-tools-version: 6.1
// This is a Skip (https://skip.tools) package.
import PackageDescription
let package = Package(
name: "pedi-foods",
defaultLocalization: "en",
platforms: [.iOS(.v17), .macOS(.v14)],
products: [
.library(name: "PediFoods", type: .dynamic, targets: ["PediFoods"]),
],
dependencies: [
.package(url: "https://source.skip.tools/skip.git", from: "1.7.0"),
.package(url: "https://source.skip.tools/skip-fuse-ui.git", from: "1.0.0")
],
targets: [
.target(name: "PediFoods", dependencies: [
.product(name: "SkipFuseUI", package: "skip-fuse-ui")
], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]),
]
)

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Darwin/PediFoods.xcodeproj">
</FileRef>
</Workspace>

60
pedi-foods/README.md Normal file
View File

@@ -0,0 +1,60 @@
# PediFoods
This is a [Skip](https://skip.tools) dual-platform app project.
<!-- TODO: add iOS screenshots to fastlane metadata
## iPhone Screenshots
<img alt="iPhone Screenshot" src="Darwin/fastlane/screenshots/en-US/1_en-US.png" style="width: 18%" /> <img alt="iPhone Screenshot" src="Darwin/fastlane/screenshots/en-US/2_en-US.png" style="width: 18%" /> <img alt="iPhone Screenshot" src="Darwin/fastlane/screenshots/en-US/3_en-US.png" style="width: 18%" /> <img alt="iPhone Screenshot" src="Darwin/fastlane/screenshots/en-US/4_en-US.png" style="width: 18%" /> <img alt="iPhone Screenshot" src="Darwin/fastlane/screenshots/en-US/5_en-US.png" style="width: 18%" />
-->
<!-- TODO: add Android screenshots to fastlane metadata
## Android Screenshots
<img alt="Android Screenshot" src="Android/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.png" style="width: 18%" /> <img alt="Android Screenshot" src="Android/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.png" style="width: 18%" /> <img alt="Android Screenshot" src="Android/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.png" style="width: 18%" /> <img alt="Android Screenshot" src="Android/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.png" style="width: 18%" /> <img alt="Android Screenshot" src="Android/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.png" style="width: 18%" />
-->
## 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.

23
pedi-foods/Skip.env Normal file
View File

@@ -0,0 +1,23 @@
// The configuration file for your Skip App (https://skip.tools).
// Properties specified here are shared between
// Darwin/PediFoods.xcconfig and Android/settings.gradle.kts
// and will be included in the app's metadata files
// Info.plist and AndroidManifest.xml
// PRODUCT_NAME is the default title of the app, which must match the app's Swift module name
PRODUCT_NAME = PediFoods
// PRODUCT_BUNDLE_IDENTIFIER is the unique id for both the iOS and Android app
PRODUCT_BUNDLE_IDENTIFIER = com.br.pedifoods.app
// The semantic version of the app
MARKETING_VERSION = 0.0.1
// The build number specifying the internal app version
CURRENT_PROJECT_VERSION = 1
// The package name for the Android entry point, referenced by the AndroidManifest.xml
ANDROID_PACKAGE_NAME = pedi.foods
// If your Android appId is different from the iOS Bundle Identifer, specify it here
// ANDROID_APPLICATION_ID = com.br.pedifoods.app

View File

@@ -0,0 +1,177 @@
import SwiftUI
enum ContentTab: String, Hashable {
case welcome, home, settings
}
struct ContentView: View {
@AppStorage("tab") var tab = ContentTab.welcome
@AppStorage("name") var welcomeName = "Skipper"
@AppStorage("appearance") var appearance = ""
@State var viewModel = ViewModel()
var body: some View {
TabView(selection: $tab) {
NavigationStack {
WelcomeView(welcomeName: $welcomeName)
}
.tabItem { Label("Welcome", systemImage: "heart.fill") }
.tag(ContentTab.welcome)
NavigationStack {
ItemListView()
.navigationTitle(Text("\(viewModel.items.count) Items"))
}
.tabItem { Label("Home", systemImage: "house.fill") }
.tag(ContentTab.home)
NavigationStack {
SettingsView(appearance: $appearance, welcomeName: $welcomeName)
.navigationTitle("Settings")
}
.tabItem { Label("Settings", systemImage: "gearshape.fill") }
.tag(ContentTab.settings)
}
.environment(viewModel)
.preferredColorScheme(appearance == "dark" ? .dark : appearance == "light" ? .light : nil)
}
}
struct WelcomeView : View {
@State var heartBeating = false
@Binding var welcomeName: String
var body: some View {
VStack(spacing: 0) {
Text("Hello [\(welcomeName)](https://skip.tools)!")
.padding()
Image(systemName: "heart.fill")
.foregroundStyle(.red)
.scaleEffect(heartBeating ? 1.5 : 1.0)
.animation(.easeInOut(duration: 1).repeatForever(), value: heartBeating)
.task { heartBeating = true }
}
.font(.largeTitle)
}
}
struct ItemListView : View {
@Environment(ViewModel.self) var viewModel: ViewModel
var body: some View {
List {
ForEach(viewModel.items) { item in
NavigationLink(value: item) {
Label {
Text(item.itemTitle)
} icon: {
if item.favorite {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
}
}
}
}
.onDelete { offsets in
viewModel.items.remove(atOffsets: offsets)
}
.onMove { fromOffsets, toOffset in
viewModel.items.move(fromOffsets: fromOffsets, toOffset: toOffset)
}
}
.navigationDestination(for: Item.self) { item in
ItemView(item: item)
.navigationTitle(item.itemTitle)
}
.toolbar {
ToolbarItemGroup {
Button {
withAnimation {
viewModel.items.insert(Item(), at: 0)
}
} label: {
Label("Add", systemImage: "plus")
}
}
}
}
}
struct ItemView : View {
@State var item: Item
@Environment(ViewModel.self) var viewModel: ViewModel
@Environment(\.dismiss) var dismiss
var body: some View {
Form {
TextField("Title", text: $item.title)
.textFieldStyle(.roundedBorder)
Toggle("Favorite", isOn: $item.favorite)
DatePicker("Date", selection: $item.date)
Text("Notes").font(.title3)
TextEditor(text: $item.notes)
.border(Color.secondary, width: 1.0)
}
.navigationBarBackButtonHidden()
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Save") {
viewModel.save(item: item)
dismiss()
}
.disabled(!viewModel.isUpdated(item))
}
}
}
}
struct SettingsView : View {
@Binding var appearance: String
@Binding var welcomeName: String
var body: some View {
Form {
TextField("Name", text: $welcomeName)
Picker("Appearance", selection: $appearance) {
Text("System").tag("")
Text("Light").tag("light")
Text("Dark").tag("dark")
}
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
Text("Version \(version) (\(buildNumber))")
}
HStack {
PlatformHeartView()
Text("Powered by [Skip](https://skip.tools)")
}
}
}
}
/// A view that shows a blue heart on iOS and a green heart on Android.
struct PlatformHeartView : View {
var body: some View {
#if os(Android)
ComposeView {
HeartComposer()
}
#else
Text(verbatim: "💙")
#endif
}
}
#if SKIP
/// Use a ContentComposer to integrate Compose content. This code will be transpiled to Kotlin.
struct HeartComposer : ContentComposer {
@Composable func Compose(context: ComposeContext) {
androidx.compose.material3.Text("💚", modifier: context.modifier)
}
}
#endif

View File

@@ -0,0 +1,59 @@
import Foundation
import SkipFuse
import SwiftUI
/// A logger for the PediFoods module.
let logger: Logger = Logger(subsystem: "com.br.pedifoods.app", category: "PediFoods")
/// The shared top-level view for the app, loaded from the platform-specific App delegates below.
///
/// The default implementation merely loads the `ContentView` for the app and logs a message.
/* SKIP @bridge */public struct PediFoodsRootView : View {
/* SKIP @bridge */public init() {
}
public var body: some View {
ContentView()
.task {
logger.info("Skip app logs are viewable in the Xcode console for iOS; Android logs can be viewed in Studio or using adb logcat")
}
}
}
/// Global application delegate functions.
///
/// These functions can update a shared observable object to communicate app state changes to interested views.
/* SKIP @bridge */public final class PediFoodsAppDelegate : Sendable {
/* SKIP @bridge */public static let shared = PediFoodsAppDelegate()
private init() {
}
/* SKIP @bridge */public func onInit() {
logger.debug("onInit")
}
/* SKIP @bridge */public func onLaunch() {
logger.debug("onLaunch")
}
/* SKIP @bridge */public func onResume() {
logger.debug("onResume")
}
/* SKIP @bridge */public func onPause() {
logger.debug("onPause")
}
/* SKIP @bridge */public func onStop() {
logger.debug("onStop")
}
/* SKIP @bridge */public func onDestroy() {
logger.debug("onDestroy")
}
/* SKIP @bridge */public func onLowMemory() {
logger.debug("onLowMemory")
}
}

View File

@@ -0,0 +1,563 @@
{
"sourceLanguage" : "en",
"strings" : {
"%lld Items" : {
"comment" : "Header title for a list that contains the number of items",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld elementos"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld éléments"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "アイテム数 %lld"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld 个条目"
}
}
}
},
"Add" : {
"comment" : "Button in items list that will cause a new item to be added",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Añadir"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ajouter"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "追加"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "添加"
}
}
}
},
"Appearance" : {
"comment" : "Settings select label for the interface style of the controls (light, dark, or default)",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Apariencia"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Apparence"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "外観"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "外观"
}
}
}
},
"Cancel" : {
"comment" : "Button title indicating that the operation should be cancelled",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cancelar"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Annuler"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "キャンセル"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "取消"
}
}
}
},
"Dark" : {
"comment" : "Menu item indicating that the appearance should be in dark mode",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Oscuro"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sombre"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "ダーク"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "深色"
}
}
}
},
"Date" : {
"comment" : "Item editor form label for the Date field",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fecha"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Date"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "日付"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "日期"
}
}
}
},
"Favorite" : {
"comment" : "Item editor title label for marking the item as a favorite",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Favorito"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Favori"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "お気に入り"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "收藏"
}
}
}
},
"Hello [%@](https://skip.tools)!" : {
"comment" : "Welcome tab contents",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "¡Hola [%@](https://skip.tools)!"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bonjour [%@](https://skip.tools)!"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "こんにちは [%@](https://skip.tools)"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "你好 [%@](https://skip.tools)"
}
}
}
},
"Home" : {
"comment" : "Tab bar item title for the Home tab",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Inicio"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Accueil"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "ホーム"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "首页"
}
}
}
},
"Light" : {
"comment" : "Menu item indicating that the appearance should be in light mode",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Claro"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Clair"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "明るい"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "浅色"
}
}
}
},
"Name" : {
"comment" : "Placeholder title for the Name field in a form",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nombre"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nom"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "名前"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "姓名"
}
}
}
},
"Notes" : {
"comment" : "Item editor form label for the Notes field",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notas"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notes"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "ノート"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "笔记"
}
}
}
},
"Powered by [Skip](https://skip.tools)" : {
"comment" : "Link markdown text for the Powered by… label",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Impulsado por [Skip](https://skip.tools)"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fonctionnalités offertes par [Skip](https://skip.tools)"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "[Skip](https://skip.tools) を使って動かしています"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "由[Skip](https://skip.tools)提供支持"
}
}
}
},
"Save" : {
"comment" : "Button title indicating that the current contents should be saved",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Guardar"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Enregistrer"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "保存"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "保存"
}
}
}
},
"Settings" : {
"comment" : "Tab bar item title for the Settings tab",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Configuración"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Paramètres"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "設定"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "设置"
}
}
}
},
"System" : {
"comment" : "Menu item indicating that the appearance should be in the default system mode",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sistema"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Système"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "システム"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "系统"
}
}
}
},
"Title" : {
"comment" : "Label for the item editor form indicating the title of the item",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Título"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Titre"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "タイトル"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "标题"
}
}
}
},
"Version %@ (%@)" : {
"comment" : "Settings label showing the current version of the app",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Version %1$@ (%2$@)"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Versión %1$@ (%2$@)"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Version %1$@ (%2$@)"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "バージョン %1$@ (%2$@)"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "版本 %1$@ (%2$@)"
}
}
}
},
"Welcome" : {
"comment" : "Tab bar item title for the Welcome tab",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bienvenido"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bienvenue"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "ようこそ"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "欢迎"
}
}
}
}
},
"version" : "1.0"
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,12 @@
# Configuration file for https://skip.tools project
#
# Kotlin dependencies and Gradle build options for this module can be configured here
#build:
# contents:
# - block: 'dependencies'
# contents:
# - 'implementation("androidx.compose.runtime:runtime")'
# this is a natively-compiled Skip Fuse module
skip:
mode: 'native'

View File

@@ -0,0 +1,93 @@
import Foundation
import Observation
import SkipFuse
/// The Observable ViewModel used by the application.
@Observable public class ViewModel {
var items: [Item] = loadItems() {
didSet { saveItems() }
}
init() {
}
func clear() {
items.removeAll()
}
func isUpdated(_ item: Item) -> Bool {
item != items.first { i in
i.id == item.id
}
}
func save(item: Item) {
items = items.map { i in
i.id == item.id ? item : i
}
}
}
/// An individual item held by the ViewModel
struct Item : Identifiable, Hashable, Codable {
let id: UUID
var date: Date
var favorite: Bool
var title: String
var notes: String
init(id: UUID = UUID(), date: Date = .now, favorite: Bool = false, title: String = "", notes: String = "") {
self.id = id
self.date = date
self.favorite = favorite
self.title = title
self.notes = notes
}
var itemTitle: String {
!title.isEmpty ? title : dateString
}
var dateString: String {
date.formatted(date: .complete, time: .omitted)
}
var dateTimeString: String {
date.formatted(date: .abbreviated, time: .shortened)
}
}
/// Utilities for defaulting and persising the items in the list
extension ViewModel {
private static let savePath = URL.applicationSupportDirectory.appendingPathComponent("appdata.json")
fileprivate static func loadItems() -> [Item] {
do {
let start = Date.now
let data = try Data(contentsOf: savePath)
defer {
let end = Date.now
logger.info("loaded \(data.count) bytes from \(Self.savePath.path) in \(end.timeIntervalSince(start)) seconds")
}
return try JSONDecoder().decode([Item].self, from: data)
} catch {
// perhaps the first launch, or the data could not be read
logger.warning("failed to load data from \(Self.savePath), using defaultItems: \(error)")
let defaultItems = (1...365).map { Date(timeIntervalSinceNow: Double($0 * 60 * 60 * 24 * -1)) }
return defaultItems.map({ Item(date: $0) })
}
}
fileprivate func saveItems() {
do {
let start = Date.now
let data = try JSONEncoder().encode(items)
try FileManager.default.createDirectory(at: URL.applicationSupportDirectory, withIntermediateDirectories: true)
try data.write(to: Self.savePath)
let end = Date.now
logger.info("saved \(data.count) bytes to \(Self.savePath.path) in \(end.timeIntervalSince(start)) seconds")
} catch {
logger.error("error saving data: \(error)")
}
}
}

82
pedifoods_app_plan.md Normal file
View File

@@ -0,0 +1,82 @@
# Plano de implementacao do app PediFoods (Skip + SwiftUI)
## 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 (obrigatoria antes das telas)
1. Criar o projeto Skip (Fuse) e estrutura base de pastas.
2. Configurar tokens do design system e tema (cores, tipografia, espacamentos, raios, sombras).
3. Implementar o service base de API:
- Camada HTTP
- Headers padrao
- Tokens por modulo (customer, store, resource)
- Tratamento de erros e rate limit
- Persistencia segura de JWT
4. Criar um coordinator de navegacao:
- Fluxo de autenticacao (login/cadastro/OTP) fora da tabbar
- Fluxo principal com tabbar (Home, Cart, Orders, Profile)
- Modais over-context (address, filtros, etc)
5. Configurar state global (sessao, perfil, cart, endereco ativo, favoritos).
6. Integrar permissions e GPS:
- Solicitar permissao ao cadastrar endereço
- Obter localizacao atual ao cadastrar endereço
- Cache de ultima localizacao valida
## Etapa 2 — Autenticacao (fora da tabbar)
1. Tela Login
2. Tela Cadastro
3. Tela OTP (8 digitos)
4. Fluxo pos-login:
- Buscar perfil
- Abrir Home com tabbar ativa
## Etapa 3 — Home e listagem de lojas (Tab Home)
1. Home com header, busca e categorias
2. Listagem de lojas (API /api/app/stores com lat/lng)
3. Filtros e busca
4. Favoritos
## Etapa 4 — Store Detail e Catalogo
1. Tela de detalhes da loja
2. Catalogo por categorias
3. Produtos com addons
4. Favoritos de loja
## Etapa 5 — Produto e carrinho
1. Tela de produto (detalhes + adicionais)
2. Carrinho (Tab Cart)
3. Atualizacao de quantidade e subtotal
## Etapa 6 — Checkout e pagamento
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 (Tab Orders)
1. Lista de pedidos
2. Detalhe do pedido
3. Tracking de status
## Etapa 8 — Perfil e enderecos (Tab Profile)
1. Perfil do usuario
2. Lista de enderecos
3. Criar/editar endereco
## Etapa 9 — Mapa e GPS
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
1. Dark mode
2. Estados vazios e erros
3. Performance e cache
4. Build iOS e Android
5. Export APK/AAB

224
pedifoods_system_design.md Normal file
View File

@@ -0,0 +1,224 @@
# 📦 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: "#8CFF2E", // ação principal, CTAs, destaque
secondary: "#388E3C", // ações secundárias, títulos
backgroundLight: "#F7F8F5",
backgroundDark: "#18230F",
textPrimary: "#333333",
textInverse: "#FFFFFF"
}
```
Uso:
- Primary → botões principais, ícones ativos, ratings
- Secondary → headers, botões secundários
- BackgroundLight → fundo global
- BackgroundDark → dark mode
---
### 1.2 🌙 Dark Theme
Dark mode nativo, não invertido.
```ts
dark: {
background: "#18230F",
surface: "rgba(255,255,255,0.05)",
textPrimary: "#FFFFFF",
textSecondary: "#A3A3A3",
primary: "#8CFF2E",
secondary: "#388E3C"
}
```
---
### 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: "1.6"
},
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
}
```
---
## 3. IMPLEMENTAÇÃO (TAILWIND-FIRST)
### 3.1 Tokens no Tailwind
```ts
theme.extend.colors = {
primary: "#8CFF2E",
secondary: "#388E3C",
backgroundLight: "#F7F8F5",
backgroundDark: "#18230F",
typographyDark: "#333333"
}
```
---
## 4. PRINCÍPIOS DO SISTEMA
- Mobile-first
- Tailwind-first
- Dark mode por classe
- Tokens semânticos
- UI energética e saudável
- Feedback visual imediato

BIN
reference_img/cart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

BIN
reference_img/cuppons.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

BIN
reference_img/home_tabs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

BIN
reference_img/login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

Some files were not shown because too many files have changed in this diff Show More