diff --git a/API_Mobile_App.md b/API_Mobile_App.md index 007a3e5..95255e0 100644 --- a/API_Mobile_App.md +++ b/API_Mobile_App.md @@ -928,3 +928,5 @@ Para dashboard da loja e análise de produto, usar: **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. + +Autor: Daniel Arantes Loverde diff --git a/API_Store_External.md b/API_Store_External.md index a7c8862..f0a4174 100644 --- a/API_Store_External.md +++ b/API_Store_External.md @@ -638,3 +638,5 @@ Assina o contrato draft atual da loja. ### 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/feature-control-bff/README.md b/feature-control-bff/README.md index ac6f863..db11f92 100644 --- a/feature-control-bff/README.md +++ b/feature-control-bff/README.md @@ -8,6 +8,11 @@ BFF mínimo para expor flags ao app sem expor `Atomenta-Token` no cliente. - `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`) @@ -22,8 +27,11 @@ Exemplo de `FEATURE_CONTROL_DEFAULTS_JSON`: ```json { - "fc.checkout_v2": { "enabled": false, "variant": "off" }, - "fc.search_ranking_v3": { "enabled": true, "variant": "on", "payload": { "model": "v3" } } + "fc.promo": { "enabled": true, "variant": "on" }, + "fc.promo-codes": { "enabled": true, "variant": "on" }, + "fc.android": { "enabled": true, "variant": "on" }, + "fc.city-aguai": { "enabled": true, "variant": "on" }, + "fc.ios": { "enabled": true, "variant": "on" } } ``` @@ -32,7 +40,7 @@ Exemplo de `FEATURE_CONTROL_DEFAULTS_JSON`: ```json { "environment": "production", - "keys": ["fc.checkout_v2", "fc.search_ranking_v3"], + "keys": ["fc.promo", "fc.promo-codes", "fc.android", "fc.city-aguai", "fc.ios"], "context": { "subjectType": "customer", "subjectId": "cust_123", @@ -58,11 +66,14 @@ Se o app enviar `Authorization: Bearer `, o BFF tenta extrair `subjectId` d "configVersion": 7, "evaluatedAt": "2026-04-16T12:00:00.000Z", "flags": { - "checkoutV2": true, - "searchRankingV3": "off" + "promo": true, + "promoCodes": true, + "android": true, + "cityAguai": true, + "ios": true }, "raw": { - "fc.checkout_v2": { + "fc.promo": { "enabled": true, "variant": "on", "payload": null, @@ -87,3 +98,5 @@ FEATURE_CONTROL_MODULE_TOKEN="550e8400-e29b-41d4-a716-44665544000b" npm start cd feature-control-bff npm test ``` + +Autor: Daniel Arantes Loverde diff --git a/feature-control-bff/tests/bff.test.mjs b/feature-control-bff/tests/bff.test.mjs index 98ba28d..fa41cc3 100644 --- a/feature-control-bff/tests/bff.test.mjs +++ b/feature-control-bff/tests/bff.test.mjs @@ -91,8 +91,9 @@ function createUpstreamStub(options = {}) { evaluatedAt: "2026-04-16T12:00:00.000Z", configVersion: 7, flags: { - "fc.checkout_v2": { enabled: true, variant: "on", payload: null, reason: "rollout" }, - "fc.search_ranking_v3": { enabled: false, variant: "off", payload: null, reason: "default" } + "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" } } } }) @@ -118,7 +119,7 @@ function createUpstreamStub(options = {}) { function bootstrapBody() { return { environment: "production", - keys: ["fc.checkout_v2", "fc.search_ranking_v3"], + keys: ["fc.promo", "fc.promo-codes", "fc.ios"], context: { subjectType: "customer", subjectId: "cust_123", @@ -151,9 +152,10 @@ test("bootstrap sucesso retorna flags simplificadas e raw", async () => { const json = await response.json(); assert.equal(json.ok, true); assert.equal(json.configVersion, 7); - assert.equal(json.flags.checkoutV2, true); - assert.equal(json.flags.searchRankingV3, "off"); - assert.ok(json.raw["fc.checkout_v2"]); + 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(); @@ -170,7 +172,7 @@ test("bootstrap com 429 devolve fallback sem quebrar cliente", async () => { ATOMENTA_ORIGIN: `http://127.0.0.1:${upstreamPort}`, FEATURE_CONTROL_MODULE_TOKEN: "token", FEATURE_CONTROL_DEFAULTS_JSON: JSON.stringify({ - "fc.checkout_v2": { enabled: false, variant: "off" } + "fc.promo": { enabled: false, variant: "off" } }) }); @@ -184,7 +186,7 @@ test("bootstrap com 429 devolve fallback sem quebrar cliente", async () => { const json = await response.json(); assert.equal(json.source, "fallback"); assert.equal(json.upstreamError.status, 429); - assert.equal(json.flags.checkoutV2, "off"); + assert.equal(json.flags.promo, "off"); } finally { await bff.stop(); await closeServer(upstream.server); @@ -234,14 +236,14 @@ test("telemetria de exposure encaminha lote para upstream", async () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ events: [ - { featureKey: "fc.checkout_v2", variant: "on", subjectType: "customer", storeId: "store_1" } + { 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.checkout_v2"); + assert.equal(upstream.state.exposurePayload.events[0].featureKey, "fc.promo"); } finally { await bff.stop(); await closeServer(upstream.server); diff --git a/logo/pedifoods.png b/logo/pedifoods.png index 45bec47..a94a4ff 100644 Binary files a/logo/pedifoods.png and b/logo/pedifoods.png differ diff --git a/logo/pedifoods_dark.png b/logo/pedifoods_dark.png index c94bd7e..a94a4ff 100644 Binary files a/logo/pedifoods_dark.png and b/logo/pedifoods_dark.png differ diff --git a/pedi-foods/Android/app/src/main/AndroidManifest.xml b/pedi-foods/Android/app/src/main/AndroidManifest.xml index 2253f6f..e0d85e6 100644 --- a/pedi-foods/Android/app/src/main/AndroidManifest.xml +++ b/pedi-foods/Android/app/src/main/AndroidManifest.xml @@ -13,12 +13,13 @@ android:name=".AndroidAppMain" android:supportsRtl="true" android:allowBackup="true" - android:icon="@mipmap/ic_launcher"> + android:icon="@mipmap/ic_launcher" + android:theme="@style/Theme.PediFoods"> diff --git a/pedi-foods/Android/app/src/main/kotlin/Main.kt b/pedi-foods/Android/app/src/main/kotlin/Main.kt index 517e668..c585807 100644 --- a/pedi-foods/Android/app/src/main/kotlin/Main.kt +++ b/pedi-foods/Android/app/src/main/kotlin/Main.kt @@ -88,6 +88,8 @@ open class MainActivity: AppCompatActivity { override fun onResume() { super.onResume() AppDelegate.shared.onResume() + requestLocationPermissionsIfNeeded() + cacheLastKnownLocation() } override fun onPause() { @@ -172,7 +174,7 @@ open class MainActivity: AppCompatActivity { } catch (_: Exception) { null } - if (candidate != null && (best == null || candidate.accuracy < best!!.accuracy)) { + if (candidate != null && (best == null || candidate.accuracy < best.accuracy)) { best = candidate } } 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 new file mode 100644 index 0000000..9e50909 --- /dev/null +++ b/pedi-foods/Android/app/src/main/res/drawable-night/launch_splash.xml @@ -0,0 +1,9 @@ + + + + + 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 new file mode 100644 index 0000000..5139fbd --- /dev/null +++ b/pedi-foods/Android/app/src/main/res/drawable/launch_splash.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/pedi-foods/Android/app/src/main/res/drawable/login_dark.png b/pedi-foods/Android/app/src/main/res/drawable/login_dark.png new file mode 100644 index 0000000..bf637d4 Binary files /dev/null and b/pedi-foods/Android/app/src/main/res/drawable/login_dark.png differ diff --git a/pedi-foods/Android/app/src/main/res/drawable/login_light.png b/pedi-foods/Android/app/src/main/res/drawable/login_light.png new file mode 100644 index 0000000..e7c5b09 Binary files /dev/null and b/pedi-foods/Android/app/src/main/res/drawable/login_light.png differ diff --git a/pedi-foods/Android/app/src/main/res/drawable/pedifoods.png b/pedi-foods/Android/app/src/main/res/drawable/pedifoods.png new file mode 100644 index 0000000..a94a4ff Binary files /dev/null and b/pedi-foods/Android/app/src/main/res/drawable/pedifoods.png differ diff --git a/pedi-foods/Android/app/src/main/res/drawable/pedifoods_dark.png b/pedi-foods/Android/app/src/main/res/drawable/pedifoods_dark.png new file mode 100644 index 0000000..a94a4ff Binary files /dev/null and b/pedi-foods/Android/app/src/main/res/drawable/pedifoods_dark.png differ diff --git a/pedi-foods/Android/app/src/main/res/drawable/pin_image_app.png b/pedi-foods/Android/app/src/main/res/drawable/pin_image_app.png new file mode 100644 index 0000000..7ed510e Binary files /dev/null and b/pedi-foods/Android/app/src/main/res/drawable/pin_image_app.png differ diff --git a/pedi-foods/Android/app/src/main/res/drawable/pin_image_app_dark.png b/pedi-foods/Android/app/src/main/res/drawable/pin_image_app_dark.png new file mode 100644 index 0000000..450d7bc Binary files /dev/null and b/pedi-foods/Android/app/src/main/res/drawable/pin_image_app_dark.png differ 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 new file mode 100644 index 0000000..889fa6c Binary files /dev/null and b/pedi-foods/Android/app/src/main/res/drawable/placeholder_product.png 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 new file mode 100644 index 0000000..6b0acbc Binary files /dev/null and b/pedi-foods/Android/app/src/main/res/drawable/tracking_canceled.png 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 new file mode 100644 index 0000000..fee3d7f Binary files /dev/null and b/pedi-foods/Android/app/src/main/res/drawable/tracking_completed.png 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 new file mode 100644 index 0000000..2d996ae Binary files /dev/null and b/pedi-foods/Android/app/src/main/res/drawable/tracking_delivering.png 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 new file mode 100644 index 0000000..a4a9344 Binary files /dev/null and b/pedi-foods/Android/app/src/main/res/drawable/tracking_pending.png 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 new file mode 100644 index 0000000..23fc9a7 Binary files /dev/null and b/pedi-foods/Android/app/src/main/res/drawable/tracking_preparing.png 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 new file mode 100644 index 0000000..fe2f0f2 Binary files /dev/null and b/pedi-foods/Android/app/src/main/res/drawable/tracking_ready.png differ 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 index 6dae27f..0b02d33 100644 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/pedi-foods/Android/app/src/main/res/mipmap-hdpi/ic_launcher.png 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 index 6dae27f..7454989 100644 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/pedi-foods/Android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png 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 index b253f0b..221ce41 100644 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/pedi-foods/Android/app/src/main/res/mipmap-mdpi/ic_launcher.png 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 index b253f0b..5acc4e6 100644 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/pedi-foods/Android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png 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 index 8d68de2..61cdf75 100644 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/pedi-foods/Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png 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 index 8d68de2..087580a 100644 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/pedi-foods/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png 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 index 19f92aa..97f2b6e 100644 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/pedi-foods/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png 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 index 19f92aa..49e99f0 100644 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/pedi-foods/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png 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 index 4b9563a..8df98eb 100644 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/pedi-foods/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png 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 index 4b9563a..8abb41e 100644 Binary files a/pedi-foods/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/pedi-foods/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png 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 new file mode 100644 index 0000000..f59b9d2 --- /dev/null +++ b/pedi-foods/Android/app/src/main/res/values-night/themes.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/pedi-foods/Android/app/src/main/res/values/colors.xml b/pedi-foods/Android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..6fd0990 --- /dev/null +++ b/pedi-foods/Android/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #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 new file mode 100644 index 0000000..fd28550 --- /dev/null +++ b/pedi-foods/Android/app/src/main/res/values/themes.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/Contents.json b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/Contents.json index 6035446..50c4762 100644 --- a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,116 +1,38 @@ { - "images": [ + "images" : [ { - "idiom": "iphone", - "size": "20x20", - "scale": "2x", - "filename": "icon_iphone_20pt@2x.png" + "filename" : "icon_pattern_1024x1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" }, { - "idiom": "iphone", - "size": "20x20", - "scale": "3x", - "filename": "icon_iphone_20pt@3x.png" + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "icon_pattern_dark_1024x1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" }, { - "idiom": "iphone", - "size": "29x29", - "scale": "2x", - "filename": "icon_iphone_29pt@2x.png" - }, - { - "idiom": "iphone", - "size": "29x29", - "scale": "3x", - "filename": "icon_iphone_29pt@3x.png" - }, - { - "idiom": "iphone", - "size": "40x40", - "scale": "2x", - "filename": "icon_iphone_40pt@2x.png" - }, - { - "idiom": "iphone", - "size": "40x40", - "scale": "3x", - "filename": "icon_iphone_40pt@3x.png" - }, - { - "idiom": "iphone", - "size": "60x60", - "scale": "2x", - "filename": "icon_iphone_60pt@2x.png" - }, - { - "idiom": "iphone", - "size": "60x60", - "scale": "3x", - "filename": "icon_iphone_60pt@3x.png" - }, - { - "idiom": "ipad", - "size": "20x20", - "scale": "1x", - "filename": "icon_ipad_20pt@1x.png" - }, - { - "idiom": "ipad", - "size": "20x20", - "scale": "2x", - "filename": "icon_ipad_20pt@2x.png" - }, - { - "idiom": "ipad", - "size": "29x29", - "scale": "1x", - "filename": "icon_ipad_29pt@1x.png" - }, - { - "idiom": "ipad", - "size": "29x29", - "scale": "2x", - "filename": "icon_ipad_29pt@2x.png" - }, - { - "idiom": "ipad", - "size": "40x40", - "scale": "1x", - "filename": "icon_ipad_40pt@1x.png" - }, - { - "idiom": "ipad", - "size": "40x40", - "scale": "2x", - "filename": "icon_ipad_40pt@2x.png" - }, - { - "idiom": "ipad", - "size": "76x76", - "scale": "1x", - "filename": "icon_ipad_76pt@1x.png" - }, - { - "idiom": "ipad", - "size": "76x76", - "scale": "2x", - "filename": "icon_ipad_76pt@2x.png" - }, - { - "idiom": "ipad", - "size": "83.5x83.5", - "scale": "2x", - "filename": "icon_ipad_83.5pt@2x.png" - }, - { - "idiom": "ios-marketing", - "size": "1024x1024", - "scale": "1x", - "filename": "icon_ios-marketing_1024pt@1x.png" + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "filename" : "icon_pattern_1024x1024 1.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" } ], - "info": { - "version": 1, - "author": "xcode" + "info" : { + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ios-marketing_1024pt@1x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ios-marketing_1024pt@1x.png deleted file mode 100644 index 2729adc..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ios-marketing_1024pt@1x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_20pt@1x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_20pt@1x.png deleted file mode 100644 index 679e384..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_20pt@1x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_20pt@2x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_20pt@2x.png deleted file mode 100644 index b92757d..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_20pt@2x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_29pt@1x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_29pt@1x.png deleted file mode 100644 index 7ffb69f..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_29pt@1x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_29pt@2x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_29pt@2x.png deleted file mode 100644 index bcf419f..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_29pt@2x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_40pt@1x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_40pt@1x.png deleted file mode 100644 index b92757d..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_40pt@1x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_40pt@2x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_40pt@2x.png deleted file mode 100644 index 75c60b1..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_40pt@2x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_76pt@1x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_76pt@1x.png deleted file mode 100644 index e0e1cb7..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_76pt@1x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_76pt@2x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_76pt@2x.png deleted file mode 100644 index f81c408..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_76pt@2x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_83.5pt@2x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_83.5pt@2x.png deleted file mode 100644 index 1d956de..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_ipad_83.5pt@2x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_20pt@2x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_20pt@2x.png deleted file mode 100644 index b92757d..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_20pt@2x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_20pt@3x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_20pt@3x.png deleted file mode 100644 index 40d5a85..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_20pt@3x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_29pt@2x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_29pt@2x.png deleted file mode 100644 index bcf419f..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_29pt@2x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_29pt@3x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_29pt@3x.png deleted file mode 100644 index ecf3804..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_29pt@3x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_40pt@2x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_40pt@2x.png deleted file mode 100644 index 75c60b1..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_40pt@2x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_40pt@3x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_40pt@3x.png deleted file mode 100644 index 2cf0dcc..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_40pt@3x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_60pt@2x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_60pt@2x.png deleted file mode 100644 index 2cf0dcc..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_60pt@2x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_60pt@3x.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_60pt@3x.png deleted file mode 100644 index 720b6d1..0000000 Binary files a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_iphone_60pt@3x.png and /dev/null differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_1024x1024 1.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_1024x1024 1.png new file mode 100644 index 0000000..55cea2c Binary files /dev/null and b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_1024x1024 1.png differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_1024x1024.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_1024x1024.png new file mode 100644 index 0000000..55cea2c Binary files /dev/null and b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_1024x1024.png differ diff --git a/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_dark_1024x1024.png b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_dark_1024x1024.png new file mode 100644 index 0000000..00271b7 Binary files /dev/null and b/pedi-foods/Darwin/Assets.xcassets/AppIcon.appiconset/icon_pattern_dark_1024x1024.png differ diff --git a/pedi-foods/Darwin/Assets.xcassets/pedifoods.imageset/pedifoods.png b/pedi-foods/Darwin/Assets.xcassets/pedifoods.imageset/pedifoods.png index 45bec47..a94a4ff 100644 Binary files a/pedi-foods/Darwin/Assets.xcassets/pedifoods.imageset/pedifoods.png and b/pedi-foods/Darwin/Assets.xcassets/pedifoods.imageset/pedifoods.png 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 index c94bd7e..a94a4ff 100644 Binary files a/pedi-foods/Darwin/Assets.xcassets/pedifoods.imageset/pedifoods_dark.png and b/pedi-foods/Darwin/Assets.xcassets/pedifoods.imageset/pedifoods_dark.png differ diff --git a/pedi-foods/Darwin/PediFoods.xcodeproj/project.pbxproj b/pedi-foods/Darwin/PediFoods.xcodeproj/project.pbxproj index 57006fe..416a597 100644 --- a/pedi-foods/Darwin/PediFoods.xcodeproj/project.pbxproj +++ b/pedi-foods/Darwin/PediFoods.xcodeproj/project.pbxproj @@ -38,7 +38,6 @@ 496EB72F2A6AE4DE00C1253A /* Skip.env */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Skip.env; path = ../Skip.env; sourceTree = ""; }; 496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = PediFoods.xcconfig; sourceTree = ""; }; 496EB72F2A6AE4DE00C1253C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; - 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 = ""; }; 49F90C312A52156300F06D93 /* Entitlements.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Entitlements.plist; sourceTree = ""; }; @@ -66,14 +65,6 @@ name = Products; sourceTree = ""; }; - 49AB54462B066A7E007B79B2 /* SkipStone */ = { - isa = PBXGroup; - children = ( - 499AB9082B0581F4005E8330 /* plugins */, - ); - name = SkipStone; - sourceTree = ""; - }; 49F90C1F2A52156200F06D93 = { isa = PBXGroup; children = ( @@ -83,7 +74,6 @@ 496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */, 493609562A6B7EAE00C401E2 /* PediFoods */, 49F90C2A2A52156200F06D93 /* App */, - 49AB54462B066A7E007B79B2 /* SkipStone */, 496BDBEC2B89A47800C09264 /* Products */, ); sourceTree = ""; @@ -110,7 +100,6 @@ 499CD43A2AC5B799001AE8D8 /* Sources */, 499CD43C2AC5B799001AE8D8 /* Frameworks */, 499CD43E2AC5B799001AE8D8 /* Resources */, - 499CD4452AC5B869001AE8D8 /* Run skip gradle */, 499CD44A2AC5B9C6001AE8D8 /* Embed Frameworks */, ); buildRules = ( @@ -173,26 +162,6 @@ }; /* 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 [ \"${PLATFORM_NAME}\" = \"iphonesimulator\" ]; then\n echo \"note: skipping skip gradle for iOS Simulator\"\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 */ diff --git a/pedi-foods/PROFILE_PLAN.md b/pedi-foods/PROFILE_PLAN.md index adc64da..b1931cf 100644 --- a/pedi-foods/PROFILE_PLAN.md +++ b/pedi-foods/PROFILE_PLAN.md @@ -1,4 +1,4 @@ -# Plano - Tela de Perfil +e# Plano - Tela de Perfil Legenda: - 🟢 Concluído @@ -28,3 +28,4 @@ Legenda: - Configurações/Ajuda (confirmar nomenclatura final) - 🔴 Corrigir build global do projeto (dependência `LCEssentials` ausente em `StoreDetailView.swift`) +Autor: Daniel Arantes Loverde diff --git a/pedi-foods/PROJECT_ANALYSIS_SKIP.md b/pedi-foods/PROJECT_ANALYSIS_SKIP.md index b7640e3..6e7b20b 100644 --- a/pedi-foods/PROJECT_ANALYSIS_SKIP.md +++ b/pedi-foods/PROJECT_ANALYSIS_SKIP.md @@ -49,3 +49,4 @@ Implicação: 1. Mudança de splash para iOS pode ser feita sem tocar Android. 2. Quando Android for tratado, deve entrar em camada própria (tema/recursos Android), sem impactar o fluxo iOS. +Autor: Daniel Arantes Loverde diff --git a/pedi-foods/Package.swift b/pedi-foods/Package.swift index 888bcd1..e63b9a8 100644 --- a/pedi-foods/Package.swift +++ b/pedi-foods/Package.swift @@ -1,5 +1,4 @@ // swift-tools-version: 6.1 -// This is a Skip (https://skip.tools) package. import PackageDescription let package = Package( @@ -10,15 +9,12 @@ let package = Package( .library(name: "PediFoods", type: .dynamic, targets: ["PediFoods"]), ], dependencies: [ - .package(url: "https://source.skip.tools/skip.git", from: "1.8.4"), - .package(path: "Vendor/skip-fuse-ui"), .package(url: "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials.git", from: "1.0.5") ], targets: [ .target(name: "PediFoods", dependencies: [ - .product(name: "SkipFuseUI", package: "skip-fuse-ui"), - .product(name: "LCEssentials", package: "lcessentials", condition: .when(platforms: [.iOS])) - ], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]), + .product(name: "LCEssentials", package: "lcessentials") + ], resources: [.process("Resources")]), .testTarget(name: "PediFoodsTests", dependencies: ["PediFoods"]), ] ) diff --git a/pedi-foods/README.md b/pedi-foods/README.md index 4581632..c663e6a 100644 --- a/pedi-foods/README.md +++ b/pedi-foods/README.md @@ -58,3 +58,5 @@ 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/Components/Buttons.swift b/pedi-foods/Sources/PediFoods/Components/Buttons.swift index ba1a964..7c02c3d 100644 --- a/pedi-foods/Sources/PediFoods/Components/Buttons.swift +++ b/pedi-foods/Sources/PediFoods/Components/Buttons.swift @@ -39,6 +39,7 @@ struct PrimaryButtonLabel: View { .frame(height: 56) .background(AppColors.primary) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) + .labelStyle(.titleOnly) } } @@ -58,7 +59,7 @@ struct SecondaryButton: View { image } } - .foregroundStyle(AppColors.textInverse) + .foregroundStyle(AppColors.textPrimary) .frame(maxWidth: fullWidth ? .infinity : nil) .frame(height: 56) .background(AppColors.secondary) diff --git a/pedi-foods/Sources/PediFoods/Components/Inputs.swift b/pedi-foods/Sources/PediFoods/Components/Inputs.swift index 9039e4e..125150c 100644 --- a/pedi-foods/Sources/PediFoods/Components/Inputs.swift +++ b/pedi-foods/Sources/PediFoods/Components/Inputs.swift @@ -7,7 +7,7 @@ struct SearchField: View { var body: some View { HStack(spacing: 12) { Image(systemName: "magnifyingglass") - .foregroundStyle(AppColors.secondary) + .foregroundStyle(AppColors.primary) TextField(placeholder, text: $text) .appNoAutoCap() } @@ -17,7 +17,7 @@ struct SearchField: View { .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous) - .stroke(AppColors.secondary.opacity(0.15), lineWidth: 1) + .stroke(AppColors.primary.opacity(0.15), lineWidth: 1) ) } } @@ -30,14 +30,14 @@ struct PillButton: View { Text(title) .font(AppTypography.caption) .tracking(AppTypography.captionLetterSpacing) - .foregroundStyle(isActive ? AppColors.textInverse : AppColors.secondary) + .foregroundStyle(isActive ? AppColors.textInverse : AppColors.primary) .padding(.horizontal, 14) .padding(.vertical, 8) .background(isActive ? AppColors.primary : AppColors.backgroundLight) .clipShape(Capsule()) .overlay( Capsule() - .stroke(AppColors.secondary.opacity(0.15), lineWidth: isActive ? 0 : 1) + .stroke(AppColors.primary.opacity(0.15), lineWidth: isActive ? 0 : 1) ) } } diff --git a/pedi-foods/Sources/PediFoods/Components/SnackbarOverlay.swift b/pedi-foods/Sources/PediFoods/Components/SnackbarOverlay.swift index 98a0bde..fda4649 100644 --- a/pedi-foods/Sources/PediFoods/Components/SnackbarOverlay.swift +++ b/pedi-foods/Sources/PediFoods/Components/SnackbarOverlay.swift @@ -1,16 +1,9 @@ import SwiftUI struct SnackbarOverlay: View { -#if os(Android) - let center: SnackbarCenter -#else @ObservedObject var center: SnackbarCenter -#endif var body: some View { -#if os(Android) - EmptyView() -#else VStack { if let message = center.current { HStack(spacing: 10) { @@ -41,6 +34,5 @@ struct SnackbarOverlay: View { } .animation(.spring(response: 0.3, dampingFraction: 0.9), value: center.current?.id) .allowsHitTesting(center.current != nil) -#endif } } diff --git a/pedi-foods/Sources/PediFoods/Components/StoreCard.swift b/pedi-foods/Sources/PediFoods/Components/StoreCard.swift index 26de91f..df66098 100644 --- a/pedi-foods/Sources/PediFoods/Components/StoreCard.swift +++ b/pedi-foods/Sources/PediFoods/Components/StoreCard.swift @@ -120,6 +120,7 @@ struct FeaturedStoreCardModel: Identifiable { let rating: Double let reviews: String let distance: String + let deliveryFee: Double? let category: String let promoText: String? let isFavorite: Bool diff --git a/pedi-foods/Sources/PediFoods/ContentView.swift b/pedi-foods/Sources/PediFoods/ContentView.swift index 5beb618..f33137f 100644 --- a/pedi-foods/Sources/PediFoods/ContentView.swift +++ b/pedi-foods/Sources/PediFoods/ContentView.swift @@ -12,16 +12,10 @@ struct ContentView: View { @State var shouldPulseLaunchSplash = true @State var shouldPrepareAuthEntryAnimation = DefaultTokenStore().jwt == nil @State var authEntryAnimationToken = 0 - #if os(iOS) @State private var sessionExpiredObserver: NSObjectProtocol? - #endif @State var cartResetObserver: Any? @State var appResumeObserver: Any? -#if os(Android) - @State var snackbarCenter = SnackbarCenter.shared -#else @StateObject var snackbarCenter = SnackbarCenter.shared -#endif var body: some View { ZStack(alignment: .top) { @@ -95,16 +89,12 @@ struct ContentView: View { .onAppear { attachCartResetObserverIfNeeded() attachAppResumeObserverIfNeeded() - #if os(iOS) attachSessionExpiredObserverIfNeeded() - #endif } .onDisappear { detachCartResetObserver() detachAppResumeObserver() - #if os(iOS) detachSessionExpiredObserver() - #endif } } @@ -402,7 +392,6 @@ struct ContentView: View { } } - #if os(iOS) private func attachSessionExpiredObserverIfNeeded() { guard sessionExpiredObserver == nil else { return } sessionExpiredObserver = NotificationCenter.default.addObserver( @@ -421,7 +410,6 @@ struct ContentView: View { NotificationCenter.default.removeObserver(observer) sessionExpiredObserver = nil } - #endif } struct AddressPickerModalView: View { diff --git a/pedi-foods/Sources/PediFoods/DesignSystem/DesignSystem.swift b/pedi-foods/Sources/PediFoods/DesignSystem/DesignSystem.swift index a644015..0d0f26c 100644 --- a/pedi-foods/Sources/PediFoods/DesignSystem/DesignSystem.swift +++ b/pedi-foods/Sources/PediFoods/DesignSystem/DesignSystem.swift @@ -1,28 +1,38 @@ import SwiftUI +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif // MARK: - Colors -enum AppColors { - static let primary = Color(hex: "#1E6B43") - static let secondary = Color(hex: "#2F7C4B") - static let tertiary = Color(hex: "#8CFF2E") +struct AppColors { + private init() {} + // Brandbook 2026 (Pedi Foods) + // Primary now follows the "FOODS" dark green tone from the logo. + static let primary = Color(.sRGB, red: 52/255.0, green: 93/255.0, blue: 84/255.0, opacity: 1.0) + // Secondary keeps the vivid lime from the symbol/logo body. + static let secondary = Color(.sRGB, red: 213/255.0, green: 216/255.0, blue: 65/255.0, opacity: 1.0) + static let tertiary = Color(.sRGB, red: 167/255.0, green: 191/255.0, blue: 66/255.0, opacity: 1.0) - static let brandDark = Color(hex: "#1B5C3A") - static let brandSoft = Color(hex: "#E8F2EC") + static let brandDark = Color(.sRGB, red: 31/255.0, green: 45/255.0, blue: 64/255.0, opacity: 1.0) + static let brandSoft = Color(.sRGB, red: 242/255.0, green: 245/255.0, blue: 227/255.0, opacity: 1.0) - static let backgroundLight = Color(hex: "#F3F5F7") - static let backgroundDark = Color(hex: "#18230F") - static let surface = Color(hex: "#FFFFFF") - static let textPrimary = Color(hex: "#1C1F23") - static let textInverse = Color(hex: "#FFFFFF") - static let textMuted = Color(hex: "#7B8794") + static let backgroundLight = Color(.sRGB, red: 243/255.0, green: 245/255.0, blue: 247/255.0, opacity: 1.0) + static let backgroundDark = Color(.sRGB, red: 31/255.0, green: 45/255.0, blue: 64/255.0, opacity: 1.0) + static let surface = Color.white + static let textPrimary = Color.black + static let textInverse = Color.white + static let textMuted = Color(.sRGB, red: 102/255.0, green: 112/255.0, blue: 133/255.0, opacity: 1.0) } -enum AppDarkColors { - static let background = Color(hex: "#18230F") - static let surface = Color(hex: "#FFFFFF", alpha: 0.05) - static let textPrimary = Color(hex: "#FFFFFF") - static let textSecondary = Color(hex: "#A3A3A3") +struct AppDarkColors { + private init() {} + static let background = Color(.sRGB, red: 31/255.0, green: 45/255.0, blue: 64/255.0, opacity: 1.0) + static let surface = Color.white.opacity(0.08) + static let textPrimary = Color.white + static let textSecondary = Color(.sRGB, red: 208/255.0, green: 213/255.0, blue: 221/255.0, opacity: 1.0) static let primary = AppColors.primary static let secondary = AppColors.secondary static let tertiary = AppColors.tertiary @@ -30,28 +40,46 @@ enum AppDarkColors { // MARK: - Typography -enum AppTypography { - static let fontFamily = "Plus Jakarta Sans" +struct AppTypography { + private init() {} + // Brandbook typography: Nexa (fallback: system font) + static let fontFamily = "Nexa-Regular" - // Avoid applying dynamic weight on custom font descriptors to prevent - // SwiftUI runtime warnings on some platforms/toolchains. - static let heading1 = Font.custom(fontFamily, size: 28) - static let heading25 = Font.custom(fontFamily, size: 25) - static let heading2 = Font.custom(fontFamily, size: 20) - static let heading3 = Font.custom(fontFamily, size: 16) - static let body = Font.custom(fontFamily, size: 16) - static let button = Font.custom(fontFamily, size: 14) - static let caption = Font.custom(fontFamily, size: 10) - static let overline = Font.custom(fontFamily, size: 11) + static let heading1 = resolvedFont(size: 28, fallbackWeight: .bold) + static let heading25 = resolvedFont(size: 25, fallbackWeight: .bold) + static let heading2 = resolvedFont(size: 20, fallbackWeight: .semibold) + static let heading3 = resolvedFont(size: 16, fallbackWeight: .semibold) + static let body = resolvedFont(size: 16, fallbackWeight: .regular) + static let button = resolvedFont(size: 14, fallbackWeight: .semibold) + static let caption = resolvedFont(size: 10, fallbackWeight: .regular) + static let overline = resolvedFont(size: 11, fallbackWeight: .regular) static let bodyLineHeight: CGFloat = 1.6 static let buttonLetterSpacing: CGFloat = 0.08 static let captionLetterSpacing: CGFloat = 0.12 + + private static func resolvedFont(size: CGFloat, fallbackWeight: Font.Weight) -> Font { + if isBrandFontAvailable { + return Font.custom(fontFamily, size: size) + } + return .system(size: size, weight: fallbackWeight, design: .default) + } + + private static var isBrandFontAvailable: Bool { + #if canImport(UIKit) + return UIFont(name: fontFamily, size: 16) != nil + #elseif canImport(AppKit) + return NSFont(name: fontFamily, size: 16) != nil + #else + return false + #endif + } } // MARK: - Layout -enum AppLayout { +struct AppLayout { + private init() {} static let radiusMD: CGFloat = 12 static let radiusLG: CGFloat = 16 static let radiusXL: CGFloat = 24 @@ -68,7 +96,8 @@ struct ShadowSpec { let y: CGFloat } -enum AppShadow { +struct AppShadow { + private init() {} static let soft = ShadowSpec(color: Color.black.opacity(0.08), radius: 20, y: 6) static let glow = ShadowSpec(color: AppColors.tertiary.opacity(0.2), radius: 24, y: 8) } @@ -79,7 +108,9 @@ extension Color { init(hex: String, alpha: Double = 1.0) { let cleaned = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int: UInt64 = 0 - Scanner(string: cleaned).scanHexInt64(&int) + if let val = UInt64(cleaned, radix: 16) { + int = val + } let r, g, b: UInt64 switch cleaned.count { diff --git a/pedi-foods/Sources/PediFoods/PediFoodsApp.swift b/pedi-foods/Sources/PediFoods/PediFoodsApp.swift index 309fc70..9b2eeea 100644 --- a/pedi-foods/Sources/PediFoods/PediFoodsApp.swift +++ b/pedi-foods/Sources/PediFoods/PediFoodsApp.swift @@ -1,60 +1,30 @@ import Foundation -import SkipFuse +import OSLog 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 struct PediFoodsRootView: View { + 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() +public final class PediFoodsAppDelegate: Sendable { + public static let shared = PediFoodsAppDelegate() - private init() { - } + private init() {} - /* SKIP @bridge */public func onInit() { - logger.debug("onInit") - } - - /* SKIP @bridge */public func onLaunch() { - logger.debug("onLaunch") - } - - /* SKIP @bridge */public func onResume() { + public func onInit() { logger.debug("onInit") } + public func onLaunch() { logger.debug("onLaunch") } + public func onResume() { logger.debug("onResume") NotificationCenter.default.post(name: .appDidResume, object: nil) } - - /* 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") - } + public func onPause() { logger.debug("onPause") } + public func onStop() { logger.debug("onStop") } + public func onDestroy() { logger.debug("onDestroy") } + public func onLowMemory() { logger.debug("onLowMemory") } } diff --git a/pedi-foods/Sources/PediFoods/Resources/Localizable.xcstrings b/pedi-foods/Sources/PediFoods/Resources/Localizable.xcstrings index b6d87f9..22f37e4 100644 --- a/pedi-foods/Sources/PediFoods/Resources/Localizable.xcstrings +++ b/pedi-foods/Sources/PediFoods/Resources/Localizable.xcstrings @@ -297,6 +297,9 @@ "Cardápio indisponível no momento." : { "comment" : "A message displayed when a store's menu is unavailable.", "isCommentAutoGenerated" : true + }, + "Carregando cardápio..." : { + }, "Carregando pedido..." : { diff --git a/pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/Assets/Screenshot 2026-04-25 at 16.22.36.png b/pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/Assets/Screenshot 2026-04-25 at 16.22.36.png new file mode 100644 index 0000000..3fefb29 Binary files /dev/null and b/pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/Assets/Screenshot 2026-04-25 at 16.22.36.png differ diff --git a/pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/Assets/Screenshot 2026-04-25 at 16.22.59.png b/pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/Assets/Screenshot 2026-04-25 at 16.22.59.png new file mode 100644 index 0000000..be0cb55 Binary files /dev/null and b/pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/Assets/Screenshot 2026-04-25 at 16.22.59.png differ diff --git a/pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/Assets/pedifoods_big.png b/pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/Assets/pedifoods_big.png deleted file mode 100644 index bd1c864..0000000 Binary files a/pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/Assets/pedifoods_big.png and /dev/null differ diff --git a/pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/icon.json b/pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/icon.json index 2dfb142..86b7994 100644 --- a/pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/icon.json +++ b/pedi-foods/Sources/PediFoods/Resources/pedifoods.icon/icon.json @@ -14,8 +14,83 @@ ], "groups" : [ { + "hidden" : false, "layers" : [ - + { + "glass-specializations" : [ + { + "appearance" : "dark", + "value" : true + } + ], + "hidden" : false, + "image-name" : "Screenshot 2026-04-25 at 16.22.36.png", + "name" : "Screenshot 2026-04-25 at 16.22.36", + "opacity-specializations" : [ + { + "value" : 0 + }, + { + "appearance" : "dark", + "value" : 1 + } + ], + "position-specializations" : [ + { + "idiom" : "iOS", + "value" : { + "scale" : 1.24, + "translation-in-points" : [ + 0, + 0 + ] + } + }, + { + "idiom" : "watchOS", + "value" : { + "scale" : 1.54, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ] + }, + { + "hidden" : false, + "image-name" : "Screenshot 2026-04-25 at 16.22.59.png", + "name" : "Screenshot 2026-04-25 at 16.22.59", + "position-specializations" : [ + { + "idiom" : "iOS", + "value" : { + "scale" : 1.24, + "translation-in-points" : [ + 0, + 0 + ] + } + }, + { + "idiom" : "watchOS", + "value" : { + "scale" : 1.54, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ] + } + ], + "opacity-specializations" : [ + { + "appearance" : "dark", + "value" : 1 + } ], "shadow" : { "kind" : "neutral", @@ -27,54 +102,18 @@ } }, { - "layers" : [ + "blend-mode" : "normal", + "hidden-specializations" : [ { - "blend-mode-specializations" : [ - { - "value" : "normal" - }, - { - "appearance" : "dark", - "value" : "normal" - } - ], - "fill-specializations" : [ - { - "value" : "none" - }, - { - "appearance" : "dark", - "value" : { - "solid" : "srgb:0.00000,0.50000,0.04260,1.00000" - } - } - ], - "glass" : false, - "hidden" : false, - "image-name" : "pedifoods_big.png", - "name" : "pedifoods_big", - "position-specializations" : [ - { - "value" : { - "scale" : 1, - "translation-in-points" : [ - 137.390625, - 11.984375 - ] - } - }, - { - "idiom" : "square", - "value" : { - "scale" : 0.35, - "translation-in-points" : [ - 8, - 0 - ] - } - } - ] + "value" : false + }, + { + "idiom" : "iOS", + "value" : false } + ], + "layers" : [ + ], "shadow" : { "kind" : "neutral", @@ -87,6 +126,9 @@ } ], "supported-platforms" : { + "circles" : [ + "watchOS" + ], "squares" : [ "iOS" ] diff --git a/pedi-foods/Sources/PediFoods/Resources/pedifoods.png b/pedi-foods/Sources/PediFoods/Resources/pedifoods.png index 45bec47..a94a4ff 100644 Binary files a/pedi-foods/Sources/PediFoods/Resources/pedifoods.png and b/pedi-foods/Sources/PediFoods/Resources/pedifoods.png differ diff --git a/pedi-foods/Sources/PediFoods/Resources/pedifoods_dark.png b/pedi-foods/Sources/PediFoods/Resources/pedifoods_dark.png index c94bd7e..a94a4ff 100644 Binary files a/pedi-foods/Sources/PediFoods/Resources/pedifoods_dark.png and b/pedi-foods/Sources/PediFoods/Resources/pedifoods_dark.png differ diff --git a/pedi-foods/Sources/PediFoods/Services/ApiOrderModels.swift b/pedi-foods/Sources/PediFoods/Services/ApiOrderModels.swift index 2e255d3..275c804 100644 --- a/pedi-foods/Sources/PediFoods/Services/ApiOrderModels.swift +++ b/pedi-foods/Sources/PediFoods/Services/ApiOrderModels.swift @@ -152,4 +152,38 @@ struct ValidateDeliveryAddressResult: Decodable { let deliveryFee: Double? let deliveryTime: String? let sameCity: Bool? + + enum CodingKeys: String, CodingKey { + case deliveryAllowed + case delivery_allowed + case reasonCode + case reason_code + case reasonMessage + case reason_message + case deliveryMode + case delivery_mode + case distance + case deliveryFee + case delivery_fee + case fee + case taxa + case deliveryTime + case delivery_time + case sameCity + case same_city + } + + init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + deliveryAllowed = (try? c.decode(Bool.self, forKey: .deliveryAllowed)) + ?? (try? c.decode(Bool.self, forKey: .delivery_allowed)) + reasonCode = ApiService.decodeFlexibleString(from: c, keys: [.reasonCode, .reason_code]) + reasonMessage = ApiService.decodeFlexibleString(from: c, keys: [.reasonMessage, .reason_message]) + deliveryMode = ApiService.decodeFlexibleString(from: c, keys: [.deliveryMode, .delivery_mode]) + distance = ApiService.decodeFlexibleDouble(from: c, keys: [.distance]) + deliveryFee = ApiService.decodeFlexibleDouble(from: c, keys: [.deliveryFee, .delivery_fee, .fee, .taxa]) + deliveryTime = ApiService.decodeFlexibleString(from: c, keys: [.deliveryTime, .delivery_time]) + sameCity = (try? c.decode(Bool.self, forKey: .sameCity)) + ?? (try? c.decode(Bool.self, forKey: .same_city)) + } } diff --git a/pedi-foods/Sources/PediFoods/Services/ApiReviewModels.swift b/pedi-foods/Sources/PediFoods/Services/ApiReviewModels.swift index 1a356f0..d4184b4 100644 --- a/pedi-foods/Sources/PediFoods/Services/ApiReviewModels.swift +++ b/pedi-foods/Sources/PediFoods/Services/ApiReviewModels.swift @@ -8,8 +8,6 @@ enum ReviewPlatform: String, Encodable { static var current: ReviewPlatform { #if os(iOS) return .ios - #elseif os(Android) - return .android #else return .web #endif diff --git a/pedi-foods/Sources/PediFoods/Services/FeatureControlService.swift b/pedi-foods/Sources/PediFoods/Services/FeatureControlService.swift index 312a157..0c77e5b 100644 --- a/pedi-foods/Sources/PediFoods/Services/FeatureControlService.swift +++ b/pedi-foods/Sources/PediFoods/Services/FeatureControlService.swift @@ -258,20 +258,16 @@ final class FeatureControlService { } private func featureKeys() -> [String] { - let raw = ProcessInfo.processInfo.environment["FEATURE_CONTROL_KEYS"] ?? "fc.checkout_v2,fc.search_ranking_v3" + let raw = ProcessInfo.processInfo.environment["FEATURE_CONTROL_KEYS"] ?? "at.city.aguai,at.promo,at.ios.only,at.android.only" let items = raw .split(separator: ",") .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { $0.isEmpty == false } - return items.isEmpty ? ["fc.checkout_v2"] : items + return items.isEmpty ? ["at.ios.only"] : items } private func platformName() -> String { - #if os(Android) - return "android" - #else return "ios" - #endif } private func appVersion() -> String { diff --git a/pedi-foods/Sources/PediFoods/Skip/skip.yml b/pedi-foods/Sources/PediFoods/Skip/skip.yml deleted file mode 100644 index fdfcd15..0000000 --- a/pedi-foods/Sources/PediFoods/Skip/skip.yml +++ /dev/null @@ -1,12 +0,0 @@ -# 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' diff --git a/pedi-foods/Sources/PediFoods/State/AppState.swift b/pedi-foods/Sources/PediFoods/State/AppState.swift index 6788b70..9d3b368 100644 --- a/pedi-foods/Sources/PediFoods/State/AppState.swift +++ b/pedi-foods/Sources/PediFoods/State/AppState.swift @@ -62,6 +62,16 @@ struct FeatureFlagsState: Codable, Equatable { var source: String = "default" var values: [String: FeatureFlagValue] = [:] var raw: [String: FeatureControlRawFlag] = [:] + + func isEnabled(_ key: String, default defaultValue: Bool = false) -> Bool { + if let rawValue = raw[key] { + return rawValue.enabled || rawValue.variant.lowercased() == "on" + } + if let mapped = values[key] { + return mapped.boolValue + } + return defaultValue + } } enum AppModal: String, Identifiable { diff --git a/pedi-foods/Sources/PediFoods/State/SnackbarCenter.swift b/pedi-foods/Sources/PediFoods/State/SnackbarCenter.swift index 093ed8e..ef52d17 100644 --- a/pedi-foods/Sources/PediFoods/State/SnackbarCenter.swift +++ b/pedi-foods/Sources/PediFoods/State/SnackbarCenter.swift @@ -1,26 +1,10 @@ import Foundation import SwiftUI -#if os(Android) -@MainActor -final class SnackbarCenter { - static let shared = SnackbarCenter() - var current: SnackbarMessage? - - func show( - title: String, - style: SnackbarStyle = .info, - icon: String? = nil, - duration: TimeInterval = 3.5, - action: (() -> Void)? = nil - ) { - action?() - } - - func handleTap() {} - func dismiss(animated: Bool) {} +extension Notification.Name { + static let snackbarDidChange = Notification.Name("snackbarDidChange") } -#else + @MainActor final class SnackbarCenter: ObservableObject { static let shared = SnackbarCenter() @@ -71,7 +55,6 @@ final class SnackbarCenter: ObservableObject { } } } -#endif enum SnackbarStyle: Sendable { case info diff --git a/pedi-foods/Sources/PediFoods/Support/PlatformCompat.swift b/pedi-foods/Sources/PediFoods/Support/PlatformCompat.swift index 092b0b3..3169da4 100644 --- a/pedi-foods/Sources/PediFoods/Support/PlatformCompat.swift +++ b/pedi-foods/Sources/PediFoods/Support/PlatformCompat.swift @@ -11,26 +11,76 @@ import CoreGraphics import UIKit #endif -#if !canImport(UIKit) -struct UIDevice { - static let topNotch = 0.0 - static let bottomNotch = 0.0 +#if canImport(AppKit) +import AppKit +#endif - var modelName: String { - "android" +#if canImport(UIKit) +extension UIDevice { + static var appSafeAreaTop: CGFloat { + let scenes = UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + let windows = scenes.flatMap { $0.windows } + if let maxTop = windows.map({ $0.safeAreaInsets.top }).max(), maxTop > 0 { + return maxTop + } + if let fallbackMaxTop = UIApplication.shared.windows.map({ $0.safeAreaInsets.top }).max(), fallbackMaxTop > 0 { + return fallbackMaxTop + } + return 0 + } +} +#else +struct UIDevice { + static let topNotch: CGFloat = 0.0 + static let bottomNotch: CGFloat = 0.0 + static let appSafeAreaTop: CGFloat = 0.0 + var modelName: String { "mac" } +} + +enum UIKeyboardType: Int { + case `default` = 0 + case asciiCapable = 1 + case numbersAndPunctuation = 2 + case URL = 3 + case numberPad = 4 + case phonePad = 10 + case namePhonePad = 9 + case emailAddress = 7 + case decimalPad = 8 + case twitter = 12 + case webSearch = 13 + case asciiCapableNumberPad = 14 +} + +extension View { + @ViewBuilder + func keyboardType(_ type: UIKeyboardType) -> some View { + self } } #endif -#if !(canImport(LCEssentials) && os(iOS)) -func printLog(title: String, msg: String) { - print("[\(title)] \(msg)") +func appReadClipboardText() -> String? { +#if canImport(UIKit) + return UIPasteboard.general.string +#elseif canImport(AppKit) + return NSPasteboard.general.string(forType: .string) +#else + return nil +#endif } -func printError(title: String, msg: String) { - print("[\(title)] \(msg)") -} +func appWriteClipboardText(_ value: String) { +#if canImport(UIKit) + UIPasteboard.general.string = value +#elseif canImport(AppKit) + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(value, forType: .string) +#else + _ = value #endif +} extension View { @ViewBuilder @@ -38,7 +88,9 @@ extension View { #if os(macOS) self #else - self.navigationBarTitleDisplayMode(.inline) + self + .navigationBarTitleDisplayMode(.inline) + .modifier(AppRoundedBackButtonModifier()) #endif } @@ -72,32 +124,48 @@ extension View { @ViewBuilder func appContentShape(_ shape: S) -> some View { -#if os(Android) - self -#else self.contentShape(shape) -#endif } @ViewBuilder func appBottomSafeAreaInset(@ViewBuilder content: () -> Content) -> some View { -#if os(Android) - self.overlay(alignment: .bottom) { - content() - } -#else self.safeAreaInset(edge: .bottom) { content() } -#endif } @ViewBuilder func appLayoutPriority(_ value: Double) -> some View { -#if os(Android) - self -#else self.layoutPriority(value) -#endif + } + + @ViewBuilder + func appNamedCoordinateSpace(_ name: String) -> some View { + self.coordinateSpace(name: name) } } + +#if !os(macOS) +private struct AppRoundedBackButtonModifier: ViewModifier { + @Environment(\.dismiss) private var dismiss + + func body(content: Content) -> some View { + content + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(action: { dismiss() }) { + Image(systemName: "chevron.left") + .font(.system(size: 24, weight: .semibold)) + .foregroundStyle(AppColors.textPrimary) + .frame(width: 52, height: 52) + .background(AppColors.surface) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.06), radius: 8, y: 2) + } + .buttonStyle(.plain) + } + } + } +} +#endif diff --git a/pedi-foods/Sources/PediFoods/Views/Auth/LegalViews.swift b/pedi-foods/Sources/PediFoods/Views/Auth/LegalViews.swift index 71e47a3..ed4d739 100644 --- a/pedi-foods/Sources/PediFoods/Views/Auth/LegalViews.swift +++ b/pedi-foods/Sources/PediFoods/Views/Auth/LegalViews.swift @@ -17,9 +17,7 @@ struct TermsOfUseView: View { } .background((colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea()) .navigationTitle("Termos de Uso") -#if os(iOS) .appInlineNavigationTitle() -#endif } } @@ -40,8 +38,6 @@ struct PrivacyPolicyView: View { } .background((colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea()) .navigationTitle("Privacidade") -#if os(iOS) .appInlineNavigationTitle() -#endif } } diff --git a/pedi-foods/Sources/PediFoods/Views/Auth/LoginEmailView.swift b/pedi-foods/Sources/PediFoods/Views/Auth/LoginEmailView.swift index c5e523c..a85de32 100644 --- a/pedi-foods/Sources/PediFoods/Views/Auth/LoginEmailView.swift +++ b/pedi-foods/Sources/PediFoods/Views/Auth/LoginEmailView.swift @@ -14,13 +14,8 @@ struct LoginEmailView: View { @Environment(\.colorScheme) var colorScheme @ViewBuilder private var logoImage: some View { -#if os(Android) SwiftUI.Image("pedifoods") .resizable() -#else - SwiftUI.Image("pedifoods") - .resizable() -#endif } var body: some View { @@ -40,9 +35,9 @@ struct LoginEmailView: View { VStack(spacing: 16) { VStack(alignment: .leading, spacing: 16) { - LoginField(icon: "envelope", placeholder: "seu@email.com", text: $email) + LoginField(icon: "envelope", placeholder: "seu@email.com", keyboardType: .emailAddress, text: $email) - LoginField(icon: "phone", placeholder: "(00) 00000-0000", text: $phone) + LoginField(icon: "phone", placeholder: "(00) 00000-0000", keyboardType: .numberPad, text: $phone) .onChange(of: phone) { _, newValue in let masked = formatPhoneBR(newValue) if masked != newValue { @@ -78,6 +73,7 @@ struct LoginEmailView: View { dismiss() } } + .buttonStyle(.plain) .font(AppTypography.body) .padding(.top, 8) @@ -125,6 +121,7 @@ struct LoginEmailView: View { struct LoginField: View { let icon: String let placeholder: String + let keyboardType: UIKeyboardType @Binding var text: String var body: some View { @@ -135,6 +132,7 @@ struct LoginField: View { TextField(text: $text, prompt: Text(placeholder).foregroundColor(Color.gray)) { } .appNoAutoCap() .foregroundColor(.black) + .keyboardType(keyboardType) } .padding(.horizontal, 16) .frame(height: 52) diff --git a/pedi-foods/Sources/PediFoods/Views/Auth/LoginView.swift b/pedi-foods/Sources/PediFoods/Views/Auth/LoginView.swift index db94ee9..6d79a9e 100644 --- a/pedi-foods/Sources/PediFoods/Views/Auth/LoginView.swift +++ b/pedi-foods/Sources/PediFoods/Views/Auth/LoginView.swift @@ -15,23 +15,13 @@ struct LoginView: View { @State var lastAnimatedToken = 0 @ViewBuilder private var logoImage: some View { -#if os(Android) SwiftUI.Image("pedifoods") .resizable() -#else - SwiftUI.Image("pedifoods") - .resizable() -#endif } @ViewBuilder private var pinHeroImage: some View { -#if os(Android) - SwiftUI.Image(colorScheme == .dark ? "pin_image_app_dark" : "pin_image_app") - .resizable() -#else SwiftUI.Image("pin_image_app") .resizable() -#endif } var body: some View { @@ -82,6 +72,7 @@ struct LoginView: View { .tint(AppColors.tertiary) .offset(y: buttonVisible ? 0 : 140) .opacity(buttonVisible ? 1.0 : 0.0) + .buttonStyle(.plain) HStack(spacing: 6) { Text("Não tem conta ainda?") @@ -89,6 +80,7 @@ struct LoginView: View { Button("Criar conta") { navigate(.registration) } + .buttonStyle(.plain) .foregroundStyle(AppColors.primary) } .font(AppTypography.body) diff --git a/pedi-foods/Sources/PediFoods/Views/Auth/OtpView.swift b/pedi-foods/Sources/PediFoods/Views/Auth/OtpView.swift index 25c8e6c..5808611 100644 --- a/pedi-foods/Sources/PediFoods/Views/Auth/OtpView.swift +++ b/pedi-foods/Sources/PediFoods/Views/Auth/OtpView.swift @@ -64,6 +64,10 @@ struct OtpView: View { if trimmed != newValue { otp = trimmed } + if trimmed.count == 8 && !isLoading { + isOtpFocused = false + validateOtp() + } } } .frame(height: 204) @@ -84,6 +88,7 @@ struct OtpView: View { Button(resendButtonTitle) { resendOtp() } + .buttonStyle(.plain) .font(AppTypography.heading3) .foregroundStyle(AppColors.primary) .disabled(isResending || !canResend) @@ -93,6 +98,7 @@ struct OtpView: View { Button("Colar código") { pasteOtpFromClipboard() } + .buttonStyle(.plain) .font(AppTypography.heading3) .foregroundStyle(AppColors.primary) .padding(.top, 22) @@ -116,6 +122,7 @@ struct OtpView: View { .padding(.bottom, 18) } } + .scrollDismissesKeyboard(.interactively) .background(AppColors.backgroundLight) .ignoresSafeArea() .onAppear { @@ -181,31 +188,20 @@ struct OtpView: View { } private func pasteOtpFromClipboard() { -#if canImport(UIKit) - let raw = UIPasteboard.general.string ?? "" + let raw = appReadClipboardText() ?? "" let digits = raw.filter(\.isNumber) let trimmed = String(digits.prefix(8)) if trimmed.isEmpty == false { otp = trimmed } -#elseif canImport(AppKit) - let raw = NSPasteboard.general.string(forType: .string) ?? "" - let digits = raw.filter(\.isNumber) - let trimmed = String(digits.prefix(8)) - if trimmed.isEmpty == false { - otp = trimmed - } -#endif } private func autoFillOtpFromClipboardIfAvailable() { -#if canImport(UIKit) guard otp.isEmpty else { return } - let raw = UIPasteboard.general.string ?? "" + let raw = appReadClipboardText() ?? "" let digits = raw.filter(\.isNumber) guard digits.count >= 8 else { return } otp = String(digits.prefix(8)) -#endif } private var otpDeliveryMessage: String { diff --git a/pedi-foods/Sources/PediFoods/Views/Auth/RegistrationView.swift b/pedi-foods/Sources/PediFoods/Views/Auth/RegistrationView.swift index 58abc04..a03f8bb 100644 --- a/pedi-foods/Sources/PediFoods/Views/Auth/RegistrationView.swift +++ b/pedi-foods/Sources/PediFoods/Views/Auth/RegistrationView.swift @@ -18,13 +18,8 @@ struct RegistrationView: View { private var isFormValid: Bool { !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !phone.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && acceptedTerms } @ViewBuilder private var logoImage: some View { -#if os(Android) SwiftUI.Image("pedifoods") .resizable() -#else - SwiftUI.Image("pedifoods") - .resizable() -#endif } var body: some View { @@ -49,9 +44,9 @@ struct RegistrationView: View { VStack(spacing: 16) { VStack(alignment: .leading, spacing: 16) { - LoginField(icon: "person", placeholder: "Ex: Maria Silva", text: $name) - LoginField(icon: "envelope", placeholder: "seu@email.com", text: $email) - LoginField(icon: "phone", placeholder: "(00) 00000-0000", text: $phone) + LoginField(icon: "person", placeholder: "Ex: Maria Silva", keyboardType: .default, text: $name) + LoginField(icon: "envelope", placeholder: "seu@email.com", keyboardType: .emailAddress, text: $email) + LoginField(icon: "phone", placeholder: "(00) 00000-0000", keyboardType: .namePhonePad, text: $phone) .onChange(of: phone) { _, newValue in let masked = formatPhoneBR(newValue) if masked != newValue { diff --git a/pedi-foods/Sources/PediFoods/Views/Launch/LaunchSplashView.swift b/pedi-foods/Sources/PediFoods/Views/Launch/LaunchSplashView.swift index 3b076d1..e05c592 100644 --- a/pedi-foods/Sources/PediFoods/Views/Launch/LaunchSplashView.swift +++ b/pedi-foods/Sources/PediFoods/Views/Launch/LaunchSplashView.swift @@ -36,12 +36,7 @@ struct LaunchSplashView: View { } @ViewBuilder var splashLogo: some View { -#if os(Android) SwiftUI.Image("pedifoods") .resizable() -#else - SwiftUI.Image("pedifoods") - .resizable() -#endif } } diff --git a/pedi-foods/Sources/PediFoods/Views/Main/AddAddressFormView.swift b/pedi-foods/Sources/PediFoods/Views/Main/AddAddressFormView.swift index 4712a44..c391380 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/AddAddressFormView.swift +++ b/pedi-foods/Sources/PediFoods/Views/Main/AddAddressFormView.swift @@ -32,13 +32,8 @@ struct AddAddressFormView: View { } @ViewBuilder private var logoImage: some View { -#if os(Android) SwiftUI.Image("pedifoods") .resizable() -#else - SwiftUI.Image("pedifoods") - .resizable() -#endif } var body: some View { @@ -47,6 +42,13 @@ struct AddAddressFormView: View { ScrollView { VStack(spacing: 0) { + screenHeader( + title: existingAddress == nil ? "Novo endereço" : "Editar endereço", + onBack: { dismiss() } + ) + .padding(.horizontal, 24) + .padding(.bottom, 12) + logoImage .scaledToFit() .frame(width: 120, height: 120) @@ -63,8 +65,8 @@ struct AddAddressFormView: View { .padding(.bottom, 20) VStack(alignment: .leading, spacing: 16) { - LoginField(icon: "tag", placeholder: "Ex: Casa, Trabalho", text: $label) - LoginField(icon: "mail", placeholder: "CEP", text: $zipCode) + LoginField(icon: "tag", placeholder: "Ex: Casa, Trabalho", keyboardType: .default, text: $label) + LoginField(icon: "mail", placeholder: "CEP", keyboardType: .numberPad, text: $zipCode) .onChange(of: zipCode) { _, newValue in let masked = formatZipCodeBR(newValue) if masked != newValue { @@ -92,12 +94,12 @@ struct AddAddressFormView: View { .foregroundStyle(AppColors.textMuted) .padding(.horizontal, 6) } - LoginField(icon: "mappin.and.ellipse", placeholder: "Rua / Avenida", text: $address) - LoginField(icon: "number", placeholder: "Número", text: $number) - LoginField(icon: "plus.app", placeholder: "Complemento (opcional)", text: $complement) - LoginField(icon: "square.grid.2x2", placeholder: "Bairro", text: $neighborhood) - LoginField(icon: "building.2", placeholder: "Cidade", text: $city) - LoginField(icon: "map", placeholder: "Estado (UF)", text: $state) + LoginField(icon: "mappin.and.ellipse", placeholder: "Rua / Avenida", keyboardType: .default, text: $address) + LoginField(icon: "number", placeholder: "Número", keyboardType: .default, text: $number) + LoginField(icon: "plus.app", placeholder: "Complemento (opcional)", keyboardType: .default, text: $complement) + LoginField(icon: "square.grid.2x2", placeholder: "Bairro", keyboardType: .default, text: $neighborhood) + LoginField(icon: "building.2", placeholder: "Cidade", keyboardType: .default, text: $city) + LoginField(icon: "map", placeholder: "Estado (UF)", keyboardType: .default, text: $state) .onChange(of: state) { _, newValue in let normalized = String(newValue.uppercased().prefix(2)) if normalized != newValue { @@ -134,6 +136,28 @@ struct AddAddressFormView: View { } } + private func screenHeader(title: String, onBack: @escaping () -> Void) -> some View { + ZStack { + Text(title) + .font(AppTypography.heading2) + .foregroundStyle(colorScheme == .dark ? Color.white : AppColors.textPrimary) + + HStack { + Button(action: onBack) { + Image(systemName: "chevron.left") + .font(.system(size: 24, weight: .semibold)) + .foregroundStyle(AppColors.textPrimary) + .frame(width: 52, height: 52) + .background(AppColors.surface) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.06), radius: 8, y: 2) + } + .buttonStyle(.plain) + Spacer() + } + } + } + private func saveAddress() { guard !isLoading else { return } let latLong: [Double]? = { diff --git a/pedi-foods/Sources/PediFoods/Views/Main/AddressesView.swift b/pedi-foods/Sources/PediFoods/Views/Main/AddressesView.swift index 5b199a5..b15b176 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/AddressesView.swift +++ b/pedi-foods/Sources/PediFoods/Views/Main/AddressesView.swift @@ -22,7 +22,7 @@ struct AddressesView: View { ScrollView(showsIndicators: false) { VStack(spacing: 20) { - header + screenHeader(title: "Meus Endereços", onBack: { dismiss() }) if let message { Text(message) @@ -131,28 +131,6 @@ struct AddressesView: View { } } - var header: some View { - ZStack { - Text("Meus Endereços") - .font(AppTypography.heading2) - .foregroundStyle(AppColors.textPrimary) - - HStack { - Button(action: { dismiss() }) { - Image(systemName: "chevron.left") - .font(.system(size: 24, weight: .semibold)) - .foregroundStyle(AppColors.textPrimary) - .frame(width: 52, height: 52) - .background(AppColors.surface) - .clipShape(Circle()) - .shadow(color: .black.opacity(0.06), radius: 8, y: 2) - } - - Spacer() - } - } - } - var bottomOverlay: some View { ZStack(alignment: .bottom) { Rectangle() @@ -181,6 +159,28 @@ struct AddressesView: View { } } + private func screenHeader(title: String, onBack: @escaping () -> Void) -> some View { + ZStack { + Text(title) + .font(AppTypography.heading2) + .foregroundStyle(AppColors.textPrimary) + + HStack { + Button(action: onBack) { + Image(systemName: "chevron.left") + .font(.system(size: 24, weight: .semibold)) + .foregroundStyle(AppColors.textPrimary) + .frame(width: 52, height: 52) + .background(AppColors.surface) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.06), radius: 8, y: 2) + } + .buttonStyle(.plain) + Spacer() + } + } + } + private func selectAddress(_ address: CustomerAddress) { appState.address.selectedId = address.id let label = (address.label ?? "").trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView.swift b/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView.swift index a7ddc49..2399d09 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView.swift +++ b/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView.swift @@ -692,12 +692,7 @@ struct PaymentPixView: View { } private func copyToClipboard(_ value: String) { -#if canImport(UIKit) - UIPasteboard.general.string = value -#elseif canImport(AppKit) - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(value, forType: .string) -#endif + appWriteClipboardText(value) } private var parsedExpirationDate: Date? { diff --git a/pedi-foods/Sources/PediFoods/Views/Main/HomeScrollOffsetObserver.swift b/pedi-foods/Sources/PediFoods/Views/Main/HomeScrollOffsetObserver.swift index c5eef3d..6dd576d 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/HomeScrollOffsetObserver.swift +++ b/pedi-foods/Sources/PediFoods/Views/Main/HomeScrollOffsetObserver.swift @@ -4,7 +4,6 @@ enum HomeScrollCoordinateSpace { static let name = "home-scroll" } -#if os(iOS) struct HomeScrollOffsetPreferenceKey: PreferenceKey { static let defaultValue: CGFloat = 0 @@ -32,4 +31,3 @@ struct ScrollOffsetObserver: View { } } } -#endif diff --git a/pedi-foods/Sources/PediFoods/Views/Main/HomeView+Filtering.swift b/pedi-foods/Sources/PediFoods/Views/Main/HomeView+Filtering.swift index 7cbf61c..25682b5 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/HomeView+Filtering.swift +++ b/pedi-foods/Sources/PediFoods/Views/Main/HomeView+Filtering.swift @@ -61,6 +61,7 @@ extension HomeView { rating: store.rating ?? 0, reviews: String(store.positiveReviews ?? store.reviewsCount ?? 0), distance: formatDistance(store.distance), + deliveryFee: store.deliveryFee, category: store.category ?? "Loja", promoText: nil, isFavorite: appState.favorites.storeIds.contains(store.id), diff --git a/pedi-foods/Sources/PediFoods/Views/Main/HomeView.swift b/pedi-foods/Sources/PediFoods/Views/Main/HomeView.swift index 2474a1d..d4e8b34 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/HomeView.swift +++ b/pedi-foods/Sources/PediFoods/Views/Main/HomeView.swift @@ -48,7 +48,7 @@ struct HomeView: View { refreshCategories: true ) } - .coordinateSpace(name: HomeScrollCoordinateSpace.name) + .appNamedCoordinateSpace(HomeScrollCoordinateSpace.name) header(collapseProgress: collapseProgress, height: headerHeight) .frame(maxWidth: .infinity, alignment: .top) @@ -101,6 +101,7 @@ struct HomeView: View { storeCategory: store.category, storeRating: store.rating, storeDistance: store.distance, + storeDeliveryFee: store.deliveryFee, appState: $appState ) } label: { @@ -121,15 +122,17 @@ struct HomeView: View { } } - section(title: "#PediPromo") { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 16) { - ForEach(specials) { item in - SpecialOfferCard(model: item) - .frame(width: 260, height: 120) + if appState.featureFlags.isEnabled("at.promo") { + section(title: "#PediPromo") { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + ForEach(specials) { item in + SpecialOfferCard(model: item) + .frame(width: 260, height: 120) + } } + .padding(.horizontal, 20) } - .padding(.horizontal, 20) } } @@ -156,6 +159,7 @@ struct HomeView: View { ) } } + .buttonStyle(.plain) .font(AppTypography.heading3) .foregroundStyle(AppColors.primary) } @@ -177,6 +181,7 @@ struct HomeView: View { storeCategory: store.category, storeRating: store.rating, storeDistance: store.distance, + storeDeliveryFee: store.deliveryFee, appState: $appState ) } label: { @@ -204,7 +209,7 @@ struct HomeView: View { return ZStack(alignment: .top) { RoundedRectangle(cornerRadius: 32, style: .continuous) - .fill(AppColors.brandDark) + .fill(AppColors.primary) .frame(height: height) .overlay(headerRings.opacity(1 - collapseProgress), alignment: .topTrailing) @@ -449,16 +454,12 @@ struct HomeView: View { @ViewBuilder private var scrollOffsetObserver: some View { -#if os(iOS) ScrollOffsetObserver { y in // Use only upward displacement for collapse and ignore top bounce. let normalized = max(0, y) scrollOffset = normalized } .frame(width: 0, height: 0) -#else - EmptyView() -#endif } private var addressCacheScope: String { diff --git a/pedi-foods/Sources/PediFoods/Views/Main/HomeViewComponents.swift b/pedi-foods/Sources/PediFoods/Views/Main/HomeViewComponents.swift index 495ce35..2a519f3 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/HomeViewComponents.swift +++ b/pedi-foods/Sources/PediFoods/Views/Main/HomeViewComponents.swift @@ -28,7 +28,7 @@ struct CategoryChip: View { .foregroundStyle(isActive ? AppColors.textInverse : AppColors.textPrimary) .padding(.horizontal, 16) .padding(.vertical, 10) - .background(isActive ? AppColors.brandDark : AppColors.surface) + .background(isActive ? AppColors.primary : AppColors.surface) .clipShape(Capsule()) } } @@ -49,6 +49,7 @@ struct SearchBar: View { Image(systemName: "slider.horizontal.3") .foregroundStyle(AppColors.textMuted) } + .buttonStyle(.plain) } .padding(.horizontal, 16) .frame(height: 52) diff --git a/pedi-foods/Sources/PediFoods/Views/Main/MainTabView.swift b/pedi-foods/Sources/PediFoods/Views/Main/MainTabView.swift index 728d850..36073e2 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/MainTabView.swift +++ b/pedi-foods/Sources/PediFoods/Views/Main/MainTabView.swift @@ -72,12 +72,12 @@ struct MainTabView: View { .font(AppTypography.heading3) } } - .foregroundStyle(isActive ? AppColors.primary : AppColors.textPrimary) + .foregroundStyle(isActive ? AppColors.textInverse : AppColors.textPrimary) .padding(.horizontal, 18) .padding(.vertical, 10) .background( Capsule() - .fill(isActive ? AppColors.brandSoft : Color.clear) + .fill(isActive ? AppColors.primary : Color.clear) ) } .buttonStyle(.plain) diff --git a/pedi-foods/Sources/PediFoods/Views/Main/OrdersView.swift b/pedi-foods/Sources/PediFoods/Views/Main/OrdersView.swift index 1325268..5be7f66 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/OrdersView.swift +++ b/pedi-foods/Sources/PediFoods/Views/Main/OrdersView.swift @@ -1,6 +1,7 @@ import SwiftUI struct OrdersView: View { + @Environment(\.dismiss) var dismiss @State var isLoading = false @State var errorMessage: String? = nil @State var orders: [AppOrderSummary] = [] @@ -12,6 +13,8 @@ struct OrdersView: View { var body: some View { ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 14) { + screenHeader(title: "Meus Pedidos", onBack: { dismiss() }) + if isLoading { ProgressView() .frame(maxWidth: .infinity) @@ -35,11 +38,12 @@ struct OrdersView: View { } } .padding(.horizontal, 20) + .padding(.top, 18) .padding(.bottom, UIDevice.bottomNotch + 18) } .background(AppColors.backgroundLight) - .navigationTitle("Meus Pedidos") - .appInlineNavigationTitle() + .navigationBarBackButtonHidden(true) + .appHiddenNavigationBar() .task { await loadOrdersIfNeeded() await refreshStoreRatings() @@ -59,6 +63,28 @@ struct OrdersView: View { } } + private func screenHeader(title: String, onBack: @escaping () -> Void) -> some View { + ZStack { + Text(title) + .font(AppTypography.heading2) + .foregroundStyle(AppColors.textPrimary) + + HStack { + Button(action: onBack) { + Image(systemName: "chevron.left") + .font(.system(size: 24, weight: .semibold)) + .foregroundStyle(AppColors.textPrimary) + .frame(width: 52, height: 52) + .background(AppColors.surface) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.06), radius: 8, y: 2) + } + .buttonStyle(.plain) + Spacer() + } + } + } + private func orderCard(_ order: AppOrderSummary) -> some View { let status = orderVisualStatus(for: order) let detailsRoute = OrderRouteContext( @@ -81,7 +107,6 @@ struct OrdersView: View { AsyncStoreImage(imageURL: resolvedMediaURL(order.storeLogoURL)) .frame(width: 80, height: 80) .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) - .background(AppColors.brandSoft, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) VStack(alignment: .leading, spacing: 5) { @@ -663,58 +688,57 @@ struct OrderEntryDestinationView: View { func resolveRoute() async { defer { isResolvingRoute = false } - if routeIntent == .tracking { - return - } - let order = await fetchOrderForRouting() guard let order else { return } + if routeIntent == .details { orderDetails = order return } - if shouldOpenOrderDetails(for: order) { + + // For both .tracking and .auto: show payment screen if payment is still pending. + // Timeline only shows once payment is confirmed or method is off-app. + if shouldOpenPaymentScreen(for: order) { + let normalizedMethod = normalizePaymentMethod(order) + if normalizedMethod.contains("PIX") { + let pixFromPayment = order.payment?.pix + let pixFromPayload = order.paymentPayload + let copyPaste = (pixFromPayment?.copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) + ? pixFromPayment?.copyPaste + : pixFromPayload?.copyPaste + let qrCodeImage = (pixFromPayment?.qrCodeImage?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) + ? pixFromPayment?.qrCodeImage + : pixFromPayload?.qrCodeImage + let expirationDate = (pixFromPayment?.expirationDate?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) + ? pixFromPayment?.expirationDate + : pixFromPayload?.expirationDate + pixContext = PixPaymentContext( + id: order.id, + orderId: order.id, + shortId: order.shortId ?? initialShortId, + copyPaste: copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false + ? (copyPaste ?? "") + : "Código PIX indisponível no momento. Aguarde e tente novamente.", + qrCodeImageBase64: qrCodeImage, + expirationDate: expirationDate + ) + return + } + if normalizedMethod == "CREDIT_CARD" || normalizedMethod == "DEBIT_CARD" { + cardContext = CardPaymentContext( + orderId: order.id, + shortId: order.shortId ?? initialShortId, + total: order.total ?? fallbackTotal ?? 0 + ) + return + } + } + + // For .auto only: route terminal/canceled orders to details instead of timeline. + if routeIntent == .auto && shouldOpenOrderDetails(for: order) { orderDetails = order - return - } - guard shouldOpenPaymentScreen(for: order) else { return } - - let normalizedMethod = normalizePaymentMethod(order) - if normalizedMethod.contains("PIX") { - let pixFromPayment = order.payment?.pix - let pixFromPayload = order.paymentPayload - let copyPaste = (pixFromPayment?.copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) - ? pixFromPayment?.copyPaste - : pixFromPayload?.copyPaste - - let qrCodeImage = (pixFromPayment?.qrCodeImage?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) - ? pixFromPayment?.qrCodeImage - : pixFromPayload?.qrCodeImage - let expirationDate = (pixFromPayment?.expirationDate?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) - ? pixFromPayment?.expirationDate - : pixFromPayload?.expirationDate - - pixContext = PixPaymentContext( - id: order.id, - orderId: order.id, - shortId: order.shortId ?? initialShortId, - copyPaste: copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false - ? (copyPaste ?? "") - : "Código PIX indisponível no momento. Aguarde e tente novamente.", - qrCodeImageBase64: qrCodeImage, - expirationDate: expirationDate - ) - return - } - - if normalizedMethod == "CREDIT_CARD" || normalizedMethod == "DEBIT_CARD" { - cardContext = CardPaymentContext( - orderId: order.id, - shortId: order.shortId ?? initialShortId, - total: order.total ?? fallbackTotal ?? 0 - ) - return } + // .tracking (and .auto fallthrough) → nil states → body renders OrderTrackingView } @MainActor diff --git a/pedi-foods/Sources/PediFoods/Views/Main/PizzaFlavorAddonsSheet.swift b/pedi-foods/Sources/PediFoods/Views/Main/PizzaFlavorAddonsSheet.swift index 3170436..bc27f6b 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/PizzaFlavorAddonsSheet.swift +++ b/pedi-foods/Sources/PediFoods/Views/Main/PizzaFlavorAddonsSheet.swift @@ -81,6 +81,7 @@ struct PizzaFlavorAddonsSheet: View { .appInlineNavigationTitle() .appTopBarTrailingToolbar { Button("Concluir") { dismiss() } + .buttonStyle(.plain) .foregroundStyle(AppColors.primary) } } diff --git a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailSupport.swift b/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailSupport.swift index 1ba5e28..c07efd1 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailSupport.swift +++ b/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailSupport.swift @@ -22,6 +22,7 @@ struct ScrollOffsetPreferenceKey: PreferenceKey { struct ScrollOffsetReader: View { @Binding var offsetY: CGFloat + @State private var baseline: CGFloat? = nil var body: some View { Color.clear @@ -35,9 +36,10 @@ struct ScrollOffsetReader: View { } ) .onPreferenceChange(ScrollOffsetPreferenceKey.self) { minY in - let normalizedOffset = max(0, -minY) - if abs(offsetY - normalizedOffset) > 0.5 { - offsetY = normalizedOffset + if baseline == nil { baseline = minY } + let offset = max(0, (baseline ?? 0) - minY) + if abs(offsetY - offset) > 0.5 { + offsetY = offset } } } @@ -61,3 +63,6 @@ struct AsyncStoreImage: View { .scaledToFill() } } + + + diff --git a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView+Components.swift b/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView+Components.swift new file mode 100644 index 0000000..41b03ee --- /dev/null +++ b/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView+Components.swift @@ -0,0 +1,348 @@ +import SwiftUI + +extension StoreDetailView { + + var topSection: some View { + ZStack(alignment: .top) { + heroSection + .frame(height: topSectionHeight) + + // Hard cut: cover cannot appear below this line. + Rectangle() + .fill(AppColors.backgroundLight) + .frame(height: max(0, topSectionHeight - coverVisibleUntilY)) + .offset(y: coverVisibleUntilY) + + summaryCard + .padding(.horizontal, 16) + .padding(.top, cardTopInset) + + storeLogoBadge + .padding(.top, cardTopInset - (storeLogoSize / 2)) + } + .frame(height: topSectionHeight) + } + + var heroSection: some View { + ZStack(alignment: .top) { + AsyncStoreImage(imageURL: resolvedURL(storeCoverURL)) + .frame(height: topSectionHeight) + .overlay( + LinearGradient( + colors: [Color.black.opacity(0.32), Color.black.opacity(0.05)], + startPoint: .top, + endPoint: .bottom + ) + ) + .ignoresSafeArea(.container, edges: .top) + + VStack(spacing: 0) { + HStack { + heroIconButton(icon: "chevron.left") { + dismiss() + } + Spacer() + heroIconButton(icon: "magnifyingglass") {} + heroIconButton( + icon: isFavoriteStore ? "heart.fill" : "heart", + foregroundStyle: isFavoriteStore ? Color.red : Color.white + ) { + Task { + await toggleFavoriteStore() + } + } + } + .padding(.horizontal, 14) + .padding(.top, UIDevice.topNotch) + + Spacer() + + Text("RESTAURANT") + .font(AppTypography.overline) + .tracking(1.8) + .foregroundStyle(Color.white.opacity(0.92)) + .padding(.bottom, 14) + } + } + } + + var storeLogoBadge: some View { + ZStack { + Circle() + .fill(AppColors.surface) + .frame(width: storeLogoSize, height: storeLogoSize) + .overlay( + Circle() + .stroke(Color.white, lineWidth: 0.1) + ) + + AsyncStoreImage(imageURL: resolvedURL(storeLogoURL)) + .frame(width: storeLogoSize - 10, height: storeLogoSize - 10) + .clipShape(Circle()) + } + .shadow(color: Color.black.opacity(0.10), radius: 8, y: 3) + } + + var summaryCard: some View { + VStack(spacing: 0) { + VStack(alignment: .leading, spacing: 14) { + HStack(alignment: .top) { + VStack(alignment: .leading, spacing: 6) { + Text(storeName) + .font(AppTypography.heading2) + .foregroundStyle(AppColors.textPrimary) + .lineLimit(2) + .padding(.top, 30) + + Text(storeSubtitle) + .font(.caption) + .foregroundStyle(AppColors.textMuted) + .lineLimit(1) + } + + Spacer() + + ratingChip + } + + HStack(spacing: 0) { + statItem(title: "DISTÂNCIA", value: distanceValueLabel) + Divider().frame(height: 34) + if let deliveryTime = info?.deliveryTime { + statItem(title: "TEMPO MIN.", value: deliveryTime+" min.") + } else { + statItem(title: "TEMPO", value: "--") + } + Divider().frame(height: 34) + statItem(title: "PED. MIN.", value: deliveryValueLabel) + } + .padding(.vertical, 4) + } + .padding(16) + + if isStoreOpen == false { + Text(closedStoreBannerText) + .font(AppTypography.heading3) + .foregroundStyle(Color.white) + .frame(maxWidth: .infinity, minHeight: closedBannerHeight) + .background(AppColors.brandDark) + } + } + .frame(height: summaryCardHeight) + .background(AppColors.surface) + .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusXL, style: .continuous)) + .shadow(color: AppShadow.soft.color, radius: AppShadow.soft.radius, y: AppShadow.soft.y) + } + + @ViewBuilder + var sectionedProducts: some View { + if isLoading { + VStack(spacing: 10) { + ProgressView() + Text("Carregando cardápio...") + .font(AppTypography.body) + .foregroundStyle(AppColors.textMuted) + } + .frame(maxWidth: .infinity, minHeight: 280, alignment: .center) + } else if let errorMessage { + VStack(alignment: .leading, spacing: 10) { + Text(errorMessage) + .font(AppTypography.body) + .foregroundStyle(AppColors.textMuted) + Button("Tentar novamente") { + Task { await loadStoreData() } + } + .font(AppTypography.heading3) + .foregroundStyle(AppColors.primary) + } + .padding(.top, 16) + } else if categories.isEmpty { + Text("Cardápio indisponível no momento.") + .font(AppTypography.body) + .foregroundStyle(AppColors.textMuted) + .padding(.top, 16) + .padding(.horizontal, 16) + .padding(.bottom, 120) + } else { + ForEach(categories, id: \.id) { category in + Text(category.name) + .font(AppTypography.heading2) + .foregroundStyle(AppColors.textPrimary) + .id(sectionAnchorId(for: category.id)) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16) + .padding(.top, 8) + .padding(.bottom, 8) + .background(AppColors.backgroundLight) + .background( + GeometryReader { geometry in + Color.clear.preference( + key: CategoryHeaderOffsetPreferenceKey.self, + value: [category.id: geometry.frame(in: .global).minY] + ) + } + ) + + VStack(spacing: 12) { + ForEach(listItems(for: category)) { item in + productCard(item, in: category) + } + } + .padding(.horizontal, 16) + .padding(.bottom, 8) + } + Color.clear.frame(height: 140) + } + } + + func categoryTabs(proxy: ScrollViewProxy, isPinned: Bool = false) -> some View { + let safeTop: CGFloat = { + guard isPinned else { return 0 } + #if canImport(UIKit) + return UIDevice.appSafeAreaTop + #else + return 0 + #endif + }() + + return ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 10) { + ForEach(categories, id: \.id) { category in + let active = selectedCategoryId == category.id + Button { + selectedCategoryId = category.id + isProgrammaticCategoryScroll = true + withAnimation(.easeInOut(duration: 0.25)) { + proxy.scrollTo(sectionAnchorId(for: category.id), anchor: .top) + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { + isProgrammaticCategoryScroll = false + } + } label: { + Text(category.name) + .font(AppTypography.heading3) + .foregroundStyle(active ? AppColors.textInverse : AppColors.textMuted) + .padding(.horizontal, 16) + .padding(.vertical, 9) + .background(active ? AppColors.primary : AppColors.surface) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 10) + } + .padding(.top, safeTop) + .background(AppColors.backgroundLight) + .animation(.easeInOut(duration: 0.15), value: isPinned) + } + + func productCard(_ item: StoreCatalogListItem, in category: StoreCatalogCategory) -> some View { + let product = item.product + let hasSelectableAddons = product.addonGroups.contains { $0.items.isEmpty == false } + + return HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 8) { + Text(item.title) + .font(AppTypography.heading3) + .foregroundStyle(AppColors.textPrimary) + .lineLimit(2) + .multilineTextAlignment(.leading) + + if let description = item.description, description.isEmpty == false { + Text(description) + .font(.caption) + .foregroundStyle(AppColors.textMuted) + .lineLimit(2) + .multilineTextAlignment(.leading) + } + + Text(listPriceLabel(for: product, in: category)) + .font(AppTypography.heading3) + .foregroundStyle(AppColors.textPrimary) + } + + Spacer() + + ZStack(alignment: .bottomTrailing) { + AsyncStoreImage(imageURL: resolvedURL(item.imageURL)) + .frame(width: 92, height: 92) + .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) + + Button { + guard isStoreOpen else { + 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 { + requestOpenPizzaSheet(categoryId: pizzaCategoryId) + return + } + if hasSelectableAddons == false { + let basePrice = product.price ?? 0 + let item = CartItemState( + id: "\(storeId)::\(product.id)::base", + productId: product.id, + storeId: storeId, + name: product.name, + imageURL: resolvedURL(product.image), + quantity: 1, + unitPrice: basePrice + ) + requestAddToCart(item) + } else { + requestOpenProductSheet(product) + } + } label: { + ZStack(alignment: .leading) { + Image(systemName: "plus") + .font(.system(size: 16, weight: .bold)) + .foregroundStyle(AppColors.textPrimary) + .frame(width: 30, height: 30) + .background(AppColors.tertiary) + .clipShape(Circle()) + + let qty = quantityInCart(for: item) + if qty > 0 { + Text("\(qty)") + .font(.system(size: 10, weight: .bold)) + .foregroundStyle(Color.white) + .padding(.horizontal, 5) + .padding(.vertical, 2) + .background(Color.red) + .clipShape(Capsule()) + .offset(x: -6, y: -10) + .zIndex(1) + } + } + .offset(x: 3, y: -3) + .frame(width: 30, height: 30) + .appContentShape(Circle()) + } + .buttonStyle(.plain) + .frame(width: 30, height: 30) + .disabled(isStoreOpen == false) + .opacity(isStoreOpen ? 1 : 0.65) + .offset(x: 7, y: 7) + } + } + .padding(12) + .background(AppColors.surface) + .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) + .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) + return + } + if item.isPizzaSummary, let pizzaCategoryId = item.pizzaCategoryId { + requestOpenPizzaSheet(categoryId: pizzaCategoryId) + return + } + guard hasSelectableAddons else { return } + requestOpenProductSheet(product) + } + } + +} diff --git a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView+Logic.swift b/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView+Logic.swift index b0dba94..1cd504e 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView+Logic.swift +++ b/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView+Logic.swift @@ -51,8 +51,8 @@ extension StoreDetailView { } var deliveryValueLabel: String { - if let minOrder = info?.minOrder { - return formatCurrency(minOrder) + if let fee = storeDeliveryFee { + return formatCurrency(fee) } return "R$ --" } diff --git a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView.swift b/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView.swift index df42bbb..5b96dd9 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView.swift +++ b/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView.swift @@ -14,6 +14,7 @@ struct StoreDetailView: View { let storeCategory: String? let storeRating: Double? let storeDistance: String? + let storeDeliveryFee: Double? @Binding var appState: AppState @Environment(\.dismiss) var dismiss @@ -33,60 +34,37 @@ struct StoreDetailView: View { @State var didLoad = false @State var categoryHeaderOffsets: [String: CGFloat] = [:] @State var isProgrammaticCategoryScroll = false - @State var scrollOffsetY: CGFloat = 0 @State var isFavoriteRequestInFlight = false + @State var scrollOffset: CGFloat = 0 - let cardTopInset: CGFloat = 168 - let summaryCardBaseHeight: CGFloat = 170 + var isCategoryTabsPinned: Bool { scrollOffset >= topSectionHeight } + + let cardTopInset: CGFloat = 180 + let summaryCardBaseHeight: CGFloat = 212 let closedBannerHeight: CGFloat = 44 let coverVisibleUntilY: CGFloat = 253 let storeLogoSize: CGFloat = 84 var body: some View { - GeometryReader { geometry in - let safeTop = geometry.safeAreaInsets.top - ScrollViewReader { proxy in - - ZStack(alignment: .top) { - AppColors.backgroundLight.ignoresSafeArea() - - ScrollView(showsIndicators: false) { -#if os(Android) - LazyVStack(spacing: 0) { - ScrollOffsetReader(offsetY: $scrollOffsetY) - topSection - - categoryTabs(proxy: proxy, safeTop: safeTop, isSticky: false) - - sectionedProducts - } -#else - LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) { - ScrollOffsetReader(offsetY: $scrollOffsetY) - topSection - - categoryTabs(proxy: proxy, safeTop: safeTop, isSticky: false) - - sectionedProducts - } -#endif - } - .refreshable { - await loadStoreData(forceRefresh: true) - } - .coordinateSpace(name: StoreDetailScrollCoordinateSpace.name) - .ignoresSafeArea(edges: .top) - } - .ignoresSafeArea(edges: .top) - .overlay(alignment: .top) { - if isCategoryTabsPinned(safeTop: safeTop) { - categoryTabs(proxy: proxy, safeTop: safeTop, isSticky: true) - .transition(.opacity) - .zIndex(20) + ScrollViewReader { proxy in + ScrollView(showsIndicators: false) { + ScrollOffsetReader(offsetY: $scrollOffset) + LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) { + topSection + Section { + sectionedProducts + } header: { + categoryTabs(proxy: proxy, isPinned: isCategoryTabsPinned) } } - .saturation(isStoreOpen ? 1 : 0) } + .coordinateSpace(name: StoreDetailScrollCoordinateSpace.name) + .refreshable { + await loadStoreData(forceRefresh: true) + } + .ignoresSafeArea(edges: .top) + .background(AppColors.backgroundLight) + .saturation(isStoreOpen ? 1 : 0) } .navigationBarBackButtonHidden(true) .appHiddenNavigationBar() @@ -180,343 +158,4 @@ struct StoreDetailView: View { syncCategoryWithScroll() } } - - private var topSection: some View { - ZStack(alignment: .top) { - heroSection - .frame(height: topSectionHeight) - - // Hard cut: cover cannot appear below this line. - Rectangle() - .fill(AppColors.backgroundLight) - .frame(height: max(0, topSectionHeight - coverVisibleUntilY)) - .offset(y: coverVisibleUntilY) - - summaryCard - .padding(.horizontal, 16) - .padding(.top, cardTopInset) - - storeLogoBadge - .padding(.top, cardTopInset - (storeLogoSize / 2)) - } - .frame(height: topSectionHeight) - } - - private var heroSection: some View { - ZStack(alignment: .top) { - AsyncStoreImage(imageURL: resolvedURL(storeCoverURL)) - .frame(height: topSectionHeight) - .overlay( - LinearGradient( - colors: [Color.black.opacity(0.32), Color.black.opacity(0.05)], - startPoint: .top, - endPoint: .bottom - ) - ) - - VStack(spacing: 0) { - HStack { - heroIconButton(icon: "chevron.left") { - dismiss() - } - Spacer() - heroIconButton(icon: "magnifyingglass") {} - heroIconButton( - icon: isFavoriteStore ? "heart.fill" : "heart", - foregroundStyle: isFavoriteStore ? Color.red : Color.white - ) { - Task { - await toggleFavoriteStore() - } - } - } - .padding(.horizontal, 14) - .padding(.top, UIDevice.topNotch) - - Spacer() - - Text("RESTAURANT") - .font(AppTypography.overline) - .tracking(1.8) - .foregroundStyle(Color.white.opacity(0.92)) - .padding(.bottom, 14) - } - } - } - - private var storeLogoBadge: some View { - ZStack { - Circle() - .fill(AppColors.surface) - .frame(width: storeLogoSize, height: storeLogoSize) - .overlay( - Circle() - .stroke(Color.white, lineWidth: 0.1) - ) - - AsyncStoreImage(imageURL: resolvedURL(storeLogoURL)) - .frame(width: storeLogoSize - 10, height: storeLogoSize - 10) - .clipShape(Circle()) - } - .shadow(color: Color.black.opacity(0.10), radius: 8, y: 3) - } - - private var summaryCard: some View { - VStack(spacing: 0) { - VStack(alignment: .leading, spacing: 14) { - HStack(alignment: .top) { - VStack(alignment: .leading, spacing: 6) { - Text(storeName) - .font(AppTypography.heading2) - .foregroundStyle(AppColors.textPrimary) - .lineLimit(2) - .padding(.top, 30) - - Text(storeSubtitle) - .font(.caption) - .foregroundStyle(AppColors.textMuted) - .lineLimit(1) - } - - Spacer() - - ratingChip - } - - HStack(spacing: 0) { - statItem(title: "DISTÂNCIA", value: distanceValueLabel) - Divider().frame(height: 34) - statItem(title: "TEMPO", value: info?.deliveryTime ?? "25-35 min") - Divider().frame(height: 34) - statItem(title: "ENTREGA", value: deliveryValueLabel) - } - .padding(.vertical, 4) - } - .padding(16) - - if isStoreOpen == false { - Text(closedStoreBannerText) - .font(AppTypography.heading3) - .foregroundStyle(Color.white) - .frame(maxWidth: .infinity, minHeight: closedBannerHeight) - .background(AppColors.brandDark) - } - } - .frame(height: summaryCardHeight) - .background(AppColors.surface) - .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusXL, style: .continuous)) - .shadow(color: AppShadow.soft.color, radius: AppShadow.soft.radius, y: AppShadow.soft.y) - } - - @ViewBuilder - private var sectionedProducts: some View { - if isLoading { - ProgressView() - .frame(maxWidth: .infinity, alignment: .center) - .padding(.top, 26) - } else if let errorMessage { - VStack(alignment: .leading, spacing: 10) { - Text(errorMessage) - .font(AppTypography.body) - .foregroundStyle(AppColors.textMuted) - Button("Tentar novamente") { - Task { await loadStoreData() } - } - .font(AppTypography.heading3) - .foregroundStyle(AppColors.primary) - } - .padding(.top, 16) - } else if categories.isEmpty { - Text("Cardápio indisponível no momento.") - .font(AppTypography.body) - .foregroundStyle(AppColors.textMuted) - .padding(.top, 16) - .padding(.horizontal, 16) - .padding(.bottom, 120) - } else { - ForEach(categories, id: \.id) { category in - Section { - VStack(spacing: 12) { - ForEach(listItems(for: category)) { item in - productCard(item, in: category) - } - } - .padding(.horizontal, 16) - .padding(.bottom, 8) - } header: { - Text(category.name) - .font(AppTypography.heading2) - .foregroundStyle(AppColors.textPrimary) - .id(sectionAnchorId(for: category.id)) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 16) - .padding(.top, 8) - .padding(.bottom, 8) - .background(AppColors.backgroundLight) - .background( - GeometryReader { geometry in - Color.clear.preference( - key: CategoryHeaderOffsetPreferenceKey.self, - value: [category.id: geometry.frame(in: .global).minY] - ) - } - ) - } - } - Color.clear.frame(height: 120) - } - } - - private func categoryTabs(proxy: ScrollViewProxy, safeTop: CGFloat, isSticky: Bool) -> some View { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 10) { - ForEach(categories, id: \.id) { category in - let active = selectedCategoryId == category.id - Button { - selectedCategoryId = category.id - isProgrammaticCategoryScroll = true - withAnimation(.easeInOut(duration: 0.25)) { - proxy.scrollTo(sectionAnchorId(for: category.id), anchor: .top) - } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { - isProgrammaticCategoryScroll = false - } - } label: { - Text(category.name) - .font(AppTypography.heading3) - .foregroundStyle(active ? AppColors.textInverse : AppColors.textMuted) - .padding(.horizontal, 16) - .padding(.vertical, 9) - .background(active ? AppColors.primary : AppColors.surface) - .clipShape(Capsule()) - } - .buttonStyle(.plain) - } - } - .padding(.horizontal, 16) - .padding(.bottom, 10) - .padding(.top, isSticky ? categoryTabsPinnedInset(safeTop: safeTop) : 10) - } - .background(AppColors.backgroundLight) - } - - private func isCategoryTabsPinned(safeTop: CGFloat) -> Bool { - scrollOffsetY >= categoryTabsPinThreshold(safeTop: safeTop) - } - - private func categoryTabsPinThreshold(safeTop: CGFloat) -> CGFloat { - max(0, topSectionHeight + 10 - safeTop) - } - - private func categoryTabsPinnedInset(safeTop: CGFloat) -> CGFloat { - max(8, safeTop - 44) - } - - private func productCard(_ item: StoreCatalogListItem, in category: StoreCatalogCategory) -> some View { - let product = item.product - let hasSelectableAddons = product.addonGroups.contains { $0.items.isEmpty == false } - - return HStack(spacing: 12) { - VStack(alignment: .leading, spacing: 8) { - Text(item.title) - .font(AppTypography.heading3) - .foregroundStyle(AppColors.textPrimary) - .lineLimit(2) - .multilineTextAlignment(.leading) - - if let description = item.description, description.isEmpty == false { - Text(description) - .font(.caption) - .foregroundStyle(AppColors.textMuted) - .lineLimit(2) - .multilineTextAlignment(.leading) - } - - Text(listPriceLabel(for: product, in: category)) - .font(AppTypography.heading3) - .foregroundStyle(AppColors.textPrimary) - } - - Spacer() - - ZStack(alignment: .bottomTrailing) { - AsyncStoreImage(imageURL: resolvedURL(item.imageURL)) - .frame(width: 92, height: 92) - .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) - - Button { - guard isStoreOpen else { - 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 { - requestOpenPizzaSheet(categoryId: pizzaCategoryId) - return - } - if hasSelectableAddons == false { - let basePrice = product.price ?? 0 - let item = CartItemState( - id: "\(storeId)::\(product.id)::base", - productId: product.id, - storeId: storeId, - name: product.name, - imageURL: resolvedURL(product.image), - quantity: 1, - unitPrice: basePrice - ) - requestAddToCart(item) - } else { - requestOpenProductSheet(product) - } - } label: { - ZStack(alignment: .leading) { - Image(systemName: "plus") - .font(.system(size: 16, weight: .bold)) - .foregroundStyle(AppColors.textPrimary) - .frame(width: 30, height: 30) - .background(AppColors.tertiary) - .clipShape(Circle()) - - let qty = quantityInCart(for: item) - if qty > 0 { - Text("\(qty)") - .font(.system(size: 10, weight: .bold)) - .foregroundStyle(Color.white) - .padding(.horizontal, 5) - .padding(.vertical, 2) - .background(Color.red) - .clipShape(Capsule()) - .offset(x: -6, y: -10) - .zIndex(1) - } - } - .offset(x: 3, y: -3) - .frame(width: 30, height: 30) - .appContentShape(Circle()) - } - .buttonStyle(.plain) - .frame(width: 30, height: 30) - .disabled(isStoreOpen == false) - .opacity(isStoreOpen ? 1 : 0.65) - .offset(x: 7, y: 7) - } - } - .padding(12) - .background(AppColors.surface) - .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) - .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) - return - } - if item.isPizzaSummary, let pizzaCategoryId = item.pizzaCategoryId { - requestOpenPizzaSheet(categoryId: pizzaCategoryId) - return - } - guard hasSelectableAddons else { return } - requestOpenProductSheet(product) - } - } - } diff --git a/pedi-foods/Sources/PediFoods/Views/Main/UserProfileView+Android.swift b/pedi-foods/Sources/PediFoods/Views/Main/UserProfileView+Android.swift deleted file mode 100644 index 8974bc4..0000000 --- a/pedi-foods/Sources/PediFoods/Views/Main/UserProfileView+Android.swift +++ /dev/null @@ -1,205 +0,0 @@ -#if os(Android) -import SwiftUI - -struct UserProfileView: View { - @Binding var appState: AppState - - @State var name: String = "" - @State var email: String = "" - @State var phone: String = "" - @State var profilePicture: String = "" - @State var isSaving = false - - var body: some View { - ScrollView(showsIndicators: false) { - VStack(spacing: 22) { - avatarSection - formSection - saveButton - } - .padding(.horizontal, 20) - .padding(.top, 18) - .padding(.bottom, UIDevice.bottomNotch + 24) - } - .background(AppColors.backgroundLight) - .navigationTitle("Meu Perfil") - .appInlineNavigationTitle() - .onAppear { - hydrateFromAppState() - } - } - - private var avatarSection: some View { - VStack(spacing: 12) { - Circle() - .fill(AppColors.brandSoft) - .frame(width: 110, height: 110) - .overlay { - Text(initials) - .font(.system(size: 32, weight: .bold)) - .foregroundStyle(AppColors.primary) - } - - Button("Remover") { - profilePicture = "" - } - .font(AppTypography.caption) - .foregroundStyle(AppColors.textMuted) - .buttonStyle(.plain) - .disabled(profilePicture.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) - } - .frame(maxWidth: .infinity) - } - - private var formSection: some View { - VStack(alignment: .leading, spacing: 14) { - textFieldSection(title: "Nome", placeholder: "Seu nome completo", text: $name) - - textFieldSection(title: "E-mail", placeholder: "seu@email.com", text: $email) - .appNoAutoCap() - - textFieldSection(title: "Telefone", placeholder: "(00) 00000-0000", text: $phone) - .onChange(of: phone) { _, newValue in - let masked = formatPhoneBR(displayPhoneDigits(newValue)) - if masked != newValue { - phone = masked - } - } - .appNoAutoCap() - } - .padding(16) - .background(AppColors.surface) - .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) - } - - private var saveButton: some View { - Button(isSaving ? "Salvando..." : "Salvar Alterações") { - Task { await saveProfile() } - } - .font(AppTypography.button) - .foregroundStyle(AppColors.textInverse) - .frame(maxWidth: .infinity, minHeight: 56) - .background(AppColors.primary) - .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) - .buttonStyle(.plain) - .disabled(isSaving || canSave == false) - .opacity((isSaving || canSave == false) ? 0.6 : 1.0) - } - - private var initials: String { - let parts = name - .trimmingCharacters(in: .whitespacesAndNewlines) - .split(separator: " ") - .prefix(2) - let letters = parts.compactMap { $0.first }.map(String.init).joined() - return letters.isEmpty ? "PF" : letters.uppercased() - } - - private var canSave: Bool { - let cleanName = name.trimmingCharacters(in: .whitespacesAndNewlines) - let cleanEmail = email.trimmingCharacters(in: .whitespacesAndNewlines) - let normalizedPhone = normalizePhoneNumberForAPI(phone) - return cleanName.isEmpty == false - && cleanEmail.isEmpty == false - && cleanEmail.contains("@") - && normalizedPhone.isEmpty == false - } - - private func hydrateFromAppState() { - name = appState.profile.name - email = appState.profile.email - phone = formatPhoneForDisplay(appState.profile.phone) - profilePicture = appState.profile.profilePicture - } - - @MainActor - private func saveProfile() async { - guard canSave else { return } - - let cleanName = name.trimmingCharacters(in: .whitespacesAndNewlines) - let cleanEmail = email.trimmingCharacters(in: .whitespacesAndNewlines) - let normalizedPhone = normalizePhoneNumberForAPI(phone) - let cleanPhoto = profilePicture.trimmingCharacters(in: .whitespacesAndNewlines) - - isSaving = true - defer { isSaving = false } - - do { - let response = try await ApiService().updateCustomerProfile( - name: cleanName, - email: cleanEmail, - phoneNumber: normalizedPhone, - profilePicture: cleanPhoto.isEmpty ? nil : cleanPhoto - ) - - if response.error { - SnackbarCenter.shared.show( - title: response.message ?? "Não foi possível atualizar seu perfil.", - style: .error, - icon: "xmark.octagon.fill", - duration: 3.0 - ) - return - } - - let customer = response.result - appState.profile.id = customer?.id ?? appState.profile.id - appState.profile.name = customer?.name ?? cleanName - appState.profile.email = customer?.email ?? cleanEmail - appState.profile.phone = customer?.phoneNumber ?? normalizedPhone - appState.profile.profilePicture = customer?.profilePicture ?? cleanPhoto - SessionStateStore.setActiveUserKey( - SessionStateStore.makeUserKey(profileId: appState.profile.id, email: appState.profile.email) - ) - - SnackbarCenter.shared.show( - title: "Perfil atualizado com sucesso.", - style: .success, - icon: "checkmark.circle.fill", - duration: 2.0 - ) - } catch { - SnackbarCenter.shared.show( - title: "Não foi possível atualizar seu perfil.", - style: .error, - icon: "xmark.octagon.fill", - duration: 3.0 - ) - } - } - - private func formatPhoneForDisplay(_ raw: String) -> String { - let digits = displayPhoneDigits(raw) - if digits.isEmpty { return "" } - return formatPhoneBR(digits) - } - - private func displayPhoneDigits(_ raw: String) -> String { - var digits = raw.filter(\.isNumber) - if digits.hasPrefix("55"), digits.count > 11 { - digits = String(digits.dropFirst(2)) - } - return String(digits.prefix(11)) - } - - private func textFieldSection(title: String, placeholder: String, text: Binding) -> some View { - VStack(alignment: .leading, spacing: 8) { - Text(title) - .font(AppTypography.caption) - .foregroundStyle(AppColors.textMuted) - - TextField(placeholder, text: text) - .font(AppTypography.body) - .foregroundStyle(AppColors.textPrimary) - .padding(.horizontal, 12) - .frame(height: 50) - .background(AppColors.backgroundLight) - .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) - .overlay( - RoundedRectangle(cornerRadius: 14, style: .continuous) - .stroke(AppColors.secondary.opacity(0.2), lineWidth: 1) - ) - } - } -} -#endif diff --git a/pedi-foods/Sources/PediFoods/Views/Main/UserProfileView.swift b/pedi-foods/Sources/PediFoods/Views/Main/UserProfileView.swift index 2e9a54c..b04cb9e 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/UserProfileView.swift +++ b/pedi-foods/Sources/PediFoods/Views/Main/UserProfileView.swift @@ -1,9 +1,6 @@ -#if !os(Android) import SwiftUI -#if canImport(PhotosUI) && os(iOS) +#if os(iOS) import PhotosUI -#endif -#if canImport(UIKit) import UIKit #endif @@ -17,8 +14,8 @@ struct UserProfileView: View { @State var profilePicture: String = "" @State var isSaving = false -#if canImport(PhotosUI) && os(iOS) - @State var selectedPhotoItem: PhotosPickerItem? +#if os(iOS) + @State private var selectedPhotoItem: PhotosPickerItem? #endif var body: some View { @@ -38,7 +35,7 @@ struct UserProfileView: View { .onAppear { hydrateFromAppState() } -#if canImport(PhotosUI) && os(iOS) +#if os(iOS) .onChange(of: selectedPhotoItem) { _, newItem in Task { await applySelectedPhoto(newItem) } } @@ -63,7 +60,7 @@ struct UserProfileView: View { } HStack(spacing: 10) { -#if canImport(PhotosUI) && os(iOS) +#if os(iOS) PhotosPicker(selection: $selectedPhotoItem, matching: .images, photoLibrary: .shared()) { Text("Trocar Foto") .font(AppTypography.caption) @@ -244,19 +241,15 @@ struct UserProfileView: View { } } -#if canImport(PhotosUI) && os(iOS) +#if os(iOS) @MainActor private func applySelectedPhoto(_ item: PhotosPickerItem?) async { guard let item else { return } do { guard let data = try await item.loadTransferable(type: Data.self) else { return } - #if canImport(UIKit) guard let image = UIImage(data: data), let jpegData = image.jpegData(compressionQuality: 0.82) else { return } profilePicture = "data:image/jpeg;base64,\(jpegData.base64EncodedString())" - #else - profilePicture = "data:image/jpeg;base64,\(data.base64EncodedString())" - #endif } catch { SnackbarCenter.shared.show( title: "Não foi possível carregar a foto selecionada.", @@ -268,4 +261,3 @@ struct UserProfileView: View { } #endif } -#endif diff --git a/pedi-foods/Vendor/skip-fuse-ui b/pedi-foods/Vendor/skip-fuse-ui new file mode 160000 index 0000000..9025fb5 --- /dev/null +++ b/pedi-foods/Vendor/skip-fuse-ui @@ -0,0 +1 @@ +Subproject commit 9025fb50e2f65d75ffa2ec39aca2b76b09c19de1 diff --git a/pedi-foods/Vendor/skip-ui b/pedi-foods/Vendor/skip-ui new file mode 160000 index 0000000..9d584e7 --- /dev/null +++ b/pedi-foods/Vendor/skip-ui @@ -0,0 +1 @@ +Subproject commit 9d584e70965af90c77938b8dc08190df75bdfb08 diff --git a/pedifoods_app_plan.md b/pedifoods_app_plan.md index a2abe68..21a601a 100644 --- a/pedifoods_app_plan.md +++ b/pedifoods_app_plan.md @@ -131,3 +131,5 @@ 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 index 546c245..9088ffd 100644 --- a/pedifoods_system_design.md +++ b/pedifoods_system_design.md @@ -179,3 +179,5 @@ Icon { active?: boolean } ``` + +Autor: Daniel Arantes Loverde diff --git a/skip_for_android_plan.md b/skip_for_android_plan.md index 3d08135..5f40205 100644 --- a/skip_for_android_plan.md +++ b/skip_for_android_plan.md @@ -290,15 +290,15 @@ Legenda: 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. +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. +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. @@ -318,3 +318,46 @@ Legenda: 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 index 26378fb..e0f274c 100644 --- a/subdomain_stores_plan.md +++ b/subdomain_stores_plan.md @@ -742,3 +742,5 @@ Implementar assim: - 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