Merge pull request 'feature/step-001' (#4) from feature/step-001 into main

Reviewed-on: Loverde-Company-LTDA/Pedi-Foods-Skip#4
This commit is contained in:
2026-06-03 14:50:19 -03:00
114 changed files with 1054 additions and 1142 deletions

View File

@@ -928,3 +928,5 @@ Para dashboard da loja e análise de produto, usar:
**Compatibilidade Store:** **Compatibilidade Store:**
- O painel `/store/reviews` segue funcional com os campos legados (`rate`, `message`, `itemFeedback`, `improvementFeedback`, `deliveryFeedback`). - 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. - Campos novos coexistem para evolução gradual sem quebrar o fluxo atual.
Autor: Daniel Arantes Loverde

View File

@@ -638,3 +638,5 @@ Assina o contrato draft atual da loja.
### 14. Visualizar PDF do Contrato ### 14. Visualizar PDF do Contrato
**GET** `/api/store/contract/:contractId/pdf` **GET** `/api/store/contract/:contractId/pdf`
Retorna o stream do arquivo PDF do contrato (draft ou assinado). Retorna o stream do arquivo PDF do contrato (draft ou assinado).
Autor: Daniel Arantes Loverde

View File

@@ -8,6 +8,11 @@ BFF mínimo para expor flags ao app sem expor `Atomenta-Token` no cliente.
- `POST /feature-control/telemetry/exposure` - `POST /feature-control/telemetry/exposure`
- `GET /health` - `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 ## Variáveis de ambiente
- `PORT` (default: `8787`) - `PORT` (default: `8787`)
@@ -22,8 +27,11 @@ Exemplo de `FEATURE_CONTROL_DEFAULTS_JSON`:
```json ```json
{ {
"fc.checkout_v2": { "enabled": false, "variant": "off" }, "fc.promo": { "enabled": true, "variant": "on" },
"fc.search_ranking_v3": { "enabled": true, "variant": "on", "payload": { "model": "v3" } } "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 ```json
{ {
"environment": "production", "environment": "production",
"keys": ["fc.checkout_v2", "fc.search_ranking_v3"], "keys": ["fc.promo", "fc.promo-codes", "fc.android", "fc.city-aguai", "fc.ios"],
"context": { "context": {
"subjectType": "customer", "subjectType": "customer",
"subjectId": "cust_123", "subjectId": "cust_123",
@@ -58,11 +66,14 @@ Se o app enviar `Authorization: Bearer <JWT>`, o BFF tenta extrair `subjectId` d
"configVersion": 7, "configVersion": 7,
"evaluatedAt": "2026-04-16T12:00:00.000Z", "evaluatedAt": "2026-04-16T12:00:00.000Z",
"flags": { "flags": {
"checkoutV2": true, "promo": true,
"searchRankingV3": "off" "promoCodes": true,
"android": true,
"cityAguai": true,
"ios": true
}, },
"raw": { "raw": {
"fc.checkout_v2": { "fc.promo": {
"enabled": true, "enabled": true,
"variant": "on", "variant": "on",
"payload": null, "payload": null,
@@ -87,3 +98,5 @@ FEATURE_CONTROL_MODULE_TOKEN="550e8400-e29b-41d4-a716-44665544000b" npm start
cd feature-control-bff cd feature-control-bff
npm test npm test
``` ```
Autor: Daniel Arantes Loverde

View File

@@ -91,8 +91,9 @@ function createUpstreamStub(options = {}) {
evaluatedAt: "2026-04-16T12:00:00.000Z", evaluatedAt: "2026-04-16T12:00:00.000Z",
configVersion: 7, configVersion: 7,
flags: { flags: {
"fc.checkout_v2": { enabled: true, variant: "on", payload: null, reason: "rollout" }, "fc.promo": { enabled: true, variant: "on", payload: null, reason: "rollout" },
"fc.search_ranking_v3": { enabled: false, variant: "off", payload: null, reason: "default" } "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() { function bootstrapBody() {
return { return {
environment: "production", environment: "production",
keys: ["fc.checkout_v2", "fc.search_ranking_v3"], keys: ["fc.promo", "fc.promo-codes", "fc.ios"],
context: { context: {
subjectType: "customer", subjectType: "customer",
subjectId: "cust_123", subjectId: "cust_123",
@@ -151,9 +152,10 @@ test("bootstrap sucesso retorna flags simplificadas e raw", async () => {
const json = await response.json(); const json = await response.json();
assert.equal(json.ok, true); assert.equal(json.ok, true);
assert.equal(json.configVersion, 7); assert.equal(json.configVersion, 7);
assert.equal(json.flags.checkoutV2, true); assert.equal(json.flags.promo, true);
assert.equal(json.flags.searchRankingV3, "off"); assert.equal(json.flags.promoCodes, true);
assert.ok(json.raw["fc.checkout_v2"]); assert.equal(json.flags.ios, true);
assert.ok(json.raw["fc.promo"]);
assert.equal(upstream.state.evaluatePayload.context.subjectId, "cust_123"); assert.equal(upstream.state.evaluatePayload.context.subjectId, "cust_123");
} finally { } finally {
await bff.stop(); 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}`, ATOMENTA_ORIGIN: `http://127.0.0.1:${upstreamPort}`,
FEATURE_CONTROL_MODULE_TOKEN: "token", FEATURE_CONTROL_MODULE_TOKEN: "token",
FEATURE_CONTROL_DEFAULTS_JSON: JSON.stringify({ 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(); const json = await response.json();
assert.equal(json.source, "fallback"); assert.equal(json.source, "fallback");
assert.equal(json.upstreamError.status, 429); assert.equal(json.upstreamError.status, 429);
assert.equal(json.flags.checkoutV2, "off"); assert.equal(json.flags.promo, "off");
} finally { } finally {
await bff.stop(); await bff.stop();
await closeServer(upstream.server); await closeServer(upstream.server);
@@ -234,14 +236,14 @@ test("telemetria de exposure encaminha lote para upstream", async () => {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
events: [ 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); assert.equal(response.status, 200);
const json = await response.json(); const json = await response.json();
assert.equal(json.ok, true); 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 { } finally {
await bff.stop(); await bff.stop();
await closeServer(upstream.server); await closeServer(upstream.server);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -13,12 +13,13 @@
android:name=".AndroidAppMain" android:name=".AndroidAppMain"
android:supportsRtl="true" android:supportsRtl="true"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher"
android:theme="@style/Theme.PediFoods">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode"
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar" android:theme="@style/Theme.PediFoods.Launch"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@@ -88,6 +88,8 @@ open class MainActivity: AppCompatActivity {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
AppDelegate.shared.onResume() AppDelegate.shared.onResume()
requestLocationPermissionsIfNeeded()
cacheLastKnownLocation()
} }
override fun onPause() { override fun onPause() {
@@ -172,7 +174,7 @@ open class MainActivity: AppCompatActivity {
} catch (_: Exception) { } catch (_: Exception) {
null null
} }
if (candidate != null && (best == null || candidate.accuracy < best!!.accuracy)) { if (candidate != null && (best == null || candidate.accuracy < best.accuracy)) {
best = candidate best = candidate
} }
} }

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background_dark" />
<item
android:drawable="@mipmap/ic_launcher"
android:gravity="center"
android:width="128dp"
android:height="128dp" />
</layer-list>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background_light" />
<item
android:drawable="@mipmap/ic_launcher"
android:gravity="center"
android:width="128dp"
android:height="128dp" />
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.PediFoods" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:windowBackground">@color/splash_background_dark</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
<style name="Theme.PediFoods.Launch" parent="Theme.PediFoods">
<item name="android:windowBackground">@drawable/launch_splash</item>
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="splash_background_light">#F6F7FB</color>
<color name="splash_background_dark">#121212</color>
</resources>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.PediFoods" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:windowBackground">@color/splash_background_light</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
<style name="Theme.PediFoods.Launch" parent="Theme.PediFoods">
<item name="android:windowBackground">@drawable/launch_splash</item>
</style>
</resources>

View File

@@ -1,116 +1,38 @@
{ {
"images": [ "images" : [
{ {
"idiom": "iphone", "filename" : "icon_pattern_1024x1024.png",
"size": "20x20", "idiom" : "universal",
"scale": "2x", "platform" : "ios",
"filename": "icon_iphone_20pt@2x.png" "size" : "1024x1024"
}, },
{ {
"idiom": "iphone", "appearances" : [
"size": "20x20", {
"scale": "3x", "appearance" : "luminosity",
"filename": "icon_iphone_20pt@3x.png" "value" : "dark"
}
],
"filename" : "icon_pattern_dark_1024x1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}, },
{ {
"idiom": "iphone", "appearances" : [
"size": "29x29", {
"scale": "2x", "appearance" : "luminosity",
"filename": "icon_iphone_29pt@2x.png" "value" : "tinted"
}, }
{ ],
"idiom": "iphone", "filename" : "icon_pattern_1024x1024 1.png",
"size": "29x29", "idiom" : "universal",
"scale": "3x", "platform" : "ios",
"filename": "icon_iphone_29pt@3x.png" "size" : "1024x1024"
},
{
"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"
} }
], ],
"info": { "info" : {
"version": 1, "author" : "xcode",
"author": "xcode" "version" : 1
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -38,7 +38,6 @@
496EB72F2A6AE4DE00C1253A /* Skip.env */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Skip.env; path = ../Skip.env; sourceTree = "<group>"; }; 496EB72F2A6AE4DE00C1253A /* Skip.env */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Skip.env; path = ../Skip.env; sourceTree = "<group>"; };
496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = PediFoods.xcconfig; sourceTree = "<group>"; }; 496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = PediFoods.xcconfig; sourceTree = "<group>"; };
496EB72F2A6AE4DE00C1253C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; }; 496EB72F2A6AE4DE00C1253C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
499AB9082B0581F4005E8330 /* plugins */ = {isa = PBXFileReference; lastKnownFileType = folder; name = plugins; path = ../../Intermediates.noindex/BuildToolPluginIntermediates; sourceTree = BUILT_PRODUCTS_DIR; };
49F90C2B2A52156200F06D93 /* Main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Main.swift; path = Sources/Main.swift; sourceTree = SOURCE_ROOT; }; 49F90C2B2A52156200F06D93 /* Main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Main.swift; path = Sources/Main.swift; sourceTree = SOURCE_ROOT; };
49F90C2F2A52156300F06D93 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 49F90C2F2A52156300F06D93 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
49F90C312A52156300F06D93 /* Entitlements.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Entitlements.plist; sourceTree = "<group>"; }; 49F90C312A52156300F06D93 /* Entitlements.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Entitlements.plist; sourceTree = "<group>"; };
@@ -66,14 +65,6 @@
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
49AB54462B066A7E007B79B2 /* SkipStone */ = {
isa = PBXGroup;
children = (
499AB9082B0581F4005E8330 /* plugins */,
);
name = SkipStone;
sourceTree = "<group>";
};
49F90C1F2A52156200F06D93 = { 49F90C1F2A52156200F06D93 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -83,7 +74,6 @@
496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */, 496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */,
493609562A6B7EAE00C401E2 /* PediFoods */, 493609562A6B7EAE00C401E2 /* PediFoods */,
49F90C2A2A52156200F06D93 /* App */, 49F90C2A2A52156200F06D93 /* App */,
49AB54462B066A7E007B79B2 /* SkipStone */,
496BDBEC2B89A47800C09264 /* Products */, 496BDBEC2B89A47800C09264 /* Products */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
@@ -110,7 +100,6 @@
499CD43A2AC5B799001AE8D8 /* Sources */, 499CD43A2AC5B799001AE8D8 /* Sources */,
499CD43C2AC5B799001AE8D8 /* Frameworks */, 499CD43C2AC5B799001AE8D8 /* Frameworks */,
499CD43E2AC5B799001AE8D8 /* Resources */, 499CD43E2AC5B799001AE8D8 /* Resources */,
499CD4452AC5B869001AE8D8 /* Run skip gradle */,
499CD44A2AC5B9C6001AE8D8 /* Embed Frameworks */, 499CD44A2AC5B9C6001AE8D8 /* Embed Frameworks */,
); );
buildRules = ( buildRules = (
@@ -173,26 +162,6 @@
}; };
/* End PBXResourcesBuildPhase section */ /* 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 */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */

View File

@@ -1,4 +1,4 @@
# Plano - Tela de Perfil e# Plano - Tela de Perfil
Legenda: Legenda:
- 🟢 Concluído - 🟢 Concluído
@@ -28,3 +28,4 @@ Legenda:
- Configurações/Ajuda (confirmar nomenclatura final) - Configurações/Ajuda (confirmar nomenclatura final)
- 🔴 Corrigir build global do projeto (dependência `LCEssentials` ausente em `StoreDetailView.swift`) - 🔴 Corrigir build global do projeto (dependência `LCEssentials` ausente em `StoreDetailView.swift`)
Autor: Daniel Arantes Loverde

View File

@@ -49,3 +49,4 @@ Implicação:
1. Mudança de splash para iOS pode ser feita sem tocar Android. 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. 2. Quando Android for tratado, deve entrar em camada própria (tema/recursos Android), sem impactar o fluxo iOS.
Autor: Daniel Arantes Loverde

View File

@@ -1,5 +1,4 @@
// swift-tools-version: 6.1 // swift-tools-version: 6.1
// This is a Skip (https://skip.tools) package.
import PackageDescription import PackageDescription
let package = Package( let package = Package(
@@ -10,15 +9,12 @@ let package = Package(
.library(name: "PediFoods", type: .dynamic, targets: ["PediFoods"]), .library(name: "PediFoods", type: .dynamic, targets: ["PediFoods"]),
], ],
dependencies: [ 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") .package(url: "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials.git", from: "1.0.5")
], ],
targets: [ targets: [
.target(name: "PediFoods", dependencies: [ .target(name: "PediFoods", dependencies: [
.product(name: "SkipFuseUI", package: "skip-fuse-ui"), .product(name: "LCEssentials", package: "lcessentials")
.product(name: "LCEssentials", package: "lcessentials", condition: .when(platforms: [.iOS])) ], resources: [.process("Resources")]),
], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]),
.testTarget(name: "PediFoodsTests", dependencies: ["PediFoods"]), .testTarget(name: "PediFoodsTests", dependencies: ["PediFoods"]),
] ]
) )

View File

@@ -58,3 +58,5 @@ Kotlin JUnit tests in the Robolectric Android simulation environment.
Parity testing can be performed with `skip test`, Parity testing can be performed with `skip test`,
which will output a table of the test results for both platforms. which will output a table of the test results for both platforms.
Autor: Daniel Arantes Loverde

View File

@@ -39,6 +39,7 @@ struct PrimaryButtonLabel: View {
.frame(height: 56) .frame(height: 56)
.background(AppColors.primary) .background(AppColors.primary)
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
.labelStyle(.titleOnly)
} }
} }
@@ -58,7 +59,7 @@ struct SecondaryButton: View {
image image
} }
} }
.foregroundStyle(AppColors.textInverse) .foregroundStyle(AppColors.textPrimary)
.frame(maxWidth: fullWidth ? .infinity : nil) .frame(maxWidth: fullWidth ? .infinity : nil)
.frame(height: 56) .frame(height: 56)
.background(AppColors.secondary) .background(AppColors.secondary)

View File

@@ -7,7 +7,7 @@ struct SearchField: View {
var body: some View { var body: some View {
HStack(spacing: 12) { HStack(spacing: 12) {
Image(systemName: "magnifyingglass") Image(systemName: "magnifyingglass")
.foregroundStyle(AppColors.secondary) .foregroundStyle(AppColors.primary)
TextField(placeholder, text: $text) TextField(placeholder, text: $text)
.appNoAutoCap() .appNoAutoCap()
} }
@@ -17,7 +17,7 @@ struct SearchField: View {
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
.overlay( .overlay(
RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous) 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) Text(title)
.font(AppTypography.caption) .font(AppTypography.caption)
.tracking(AppTypography.captionLetterSpacing) .tracking(AppTypography.captionLetterSpacing)
.foregroundStyle(isActive ? AppColors.textInverse : AppColors.secondary) .foregroundStyle(isActive ? AppColors.textInverse : AppColors.primary)
.padding(.horizontal, 14) .padding(.horizontal, 14)
.padding(.vertical, 8) .padding(.vertical, 8)
.background(isActive ? AppColors.primary : AppColors.backgroundLight) .background(isActive ? AppColors.primary : AppColors.backgroundLight)
.clipShape(Capsule()) .clipShape(Capsule())
.overlay( .overlay(
Capsule() Capsule()
.stroke(AppColors.secondary.opacity(0.15), lineWidth: isActive ? 0 : 1) .stroke(AppColors.primary.opacity(0.15), lineWidth: isActive ? 0 : 1)
) )
} }
} }

View File

@@ -1,16 +1,9 @@
import SwiftUI import SwiftUI
struct SnackbarOverlay: View { struct SnackbarOverlay: View {
#if os(Android)
let center: SnackbarCenter
#else
@ObservedObject var center: SnackbarCenter @ObservedObject var center: SnackbarCenter
#endif
var body: some View { var body: some View {
#if os(Android)
EmptyView()
#else
VStack { VStack {
if let message = center.current { if let message = center.current {
HStack(spacing: 10) { HStack(spacing: 10) {
@@ -41,6 +34,5 @@ struct SnackbarOverlay: View {
} }
.animation(.spring(response: 0.3, dampingFraction: 0.9), value: center.current?.id) .animation(.spring(response: 0.3, dampingFraction: 0.9), value: center.current?.id)
.allowsHitTesting(center.current != nil) .allowsHitTesting(center.current != nil)
#endif
} }
} }

View File

@@ -120,6 +120,7 @@ struct FeaturedStoreCardModel: Identifiable {
let rating: Double let rating: Double
let reviews: String let reviews: String
let distance: String let distance: String
let deliveryFee: Double?
let category: String let category: String
let promoText: String? let promoText: String?
let isFavorite: Bool let isFavorite: Bool

View File

@@ -12,16 +12,10 @@ struct ContentView: View {
@State var shouldPulseLaunchSplash = true @State var shouldPulseLaunchSplash = true
@State var shouldPrepareAuthEntryAnimation = DefaultTokenStore().jwt == nil @State var shouldPrepareAuthEntryAnimation = DefaultTokenStore().jwt == nil
@State var authEntryAnimationToken = 0 @State var authEntryAnimationToken = 0
#if os(iOS)
@State private var sessionExpiredObserver: NSObjectProtocol? @State private var sessionExpiredObserver: NSObjectProtocol?
#endif
@State var cartResetObserver: Any? @State var cartResetObserver: Any?
@State var appResumeObserver: Any? @State var appResumeObserver: Any?
#if os(Android)
@State var snackbarCenter = SnackbarCenter.shared
#else
@StateObject var snackbarCenter = SnackbarCenter.shared @StateObject var snackbarCenter = SnackbarCenter.shared
#endif
var body: some View { var body: some View {
ZStack(alignment: .top) { ZStack(alignment: .top) {
@@ -95,16 +89,12 @@ struct ContentView: View {
.onAppear { .onAppear {
attachCartResetObserverIfNeeded() attachCartResetObserverIfNeeded()
attachAppResumeObserverIfNeeded() attachAppResumeObserverIfNeeded()
#if os(iOS)
attachSessionExpiredObserverIfNeeded() attachSessionExpiredObserverIfNeeded()
#endif
} }
.onDisappear { .onDisappear {
detachCartResetObserver() detachCartResetObserver()
detachAppResumeObserver() detachAppResumeObserver()
#if os(iOS)
detachSessionExpiredObserver() detachSessionExpiredObserver()
#endif
} }
} }
@@ -402,7 +392,6 @@ struct ContentView: View {
} }
} }
#if os(iOS)
private func attachSessionExpiredObserverIfNeeded() { private func attachSessionExpiredObserverIfNeeded() {
guard sessionExpiredObserver == nil else { return } guard sessionExpiredObserver == nil else { return }
sessionExpiredObserver = NotificationCenter.default.addObserver( sessionExpiredObserver = NotificationCenter.default.addObserver(
@@ -421,7 +410,6 @@ struct ContentView: View {
NotificationCenter.default.removeObserver(observer) NotificationCenter.default.removeObserver(observer)
sessionExpiredObserver = nil sessionExpiredObserver = nil
} }
#endif
} }
struct AddressPickerModalView: View { struct AddressPickerModalView: View {

View File

@@ -1,28 +1,38 @@
import SwiftUI import SwiftUI
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
// MARK: - Colors // MARK: - Colors
enum AppColors { struct AppColors {
static let primary = Color(hex: "#1E6B43") private init() {}
static let secondary = Color(hex: "#2F7C4B") // Brandbook 2026 (Pedi Foods)
static let tertiary = Color(hex: "#8CFF2E") // 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 brandDark = Color(.sRGB, red: 31/255.0, green: 45/255.0, blue: 64/255.0, opacity: 1.0)
static let brandSoft = Color(hex: "#E8F2EC") 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 backgroundLight = Color(.sRGB, red: 243/255.0, green: 245/255.0, blue: 247/255.0, opacity: 1.0)
static let backgroundDark = Color(hex: "#18230F") static let backgroundDark = Color(.sRGB, red: 31/255.0, green: 45/255.0, blue: 64/255.0, opacity: 1.0)
static let surface = Color(hex: "#FFFFFF") static let surface = Color.white
static let textPrimary = Color(hex: "#1C1F23") static let textPrimary = Color.black
static let textInverse = Color(hex: "#FFFFFF") static let textInverse = Color.white
static let textMuted = Color(hex: "#7B8794") static let textMuted = Color(.sRGB, red: 102/255.0, green: 112/255.0, blue: 133/255.0, opacity: 1.0)
} }
enum AppDarkColors { struct AppDarkColors {
static let background = Color(hex: "#18230F") private init() {}
static let surface = Color(hex: "#FFFFFF", alpha: 0.05) static let background = Color(.sRGB, red: 31/255.0, green: 45/255.0, blue: 64/255.0, opacity: 1.0)
static let textPrimary = Color(hex: "#FFFFFF") static let surface = Color.white.opacity(0.08)
static let textSecondary = Color(hex: "#A3A3A3") 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 primary = AppColors.primary
static let secondary = AppColors.secondary static let secondary = AppColors.secondary
static let tertiary = AppColors.tertiary static let tertiary = AppColors.tertiary
@@ -30,28 +40,46 @@ enum AppDarkColors {
// MARK: - Typography // MARK: - Typography
enum AppTypography { struct AppTypography {
static let fontFamily = "Plus Jakarta Sans" private init() {}
// Brandbook typography: Nexa (fallback: system font)
static let fontFamily = "Nexa-Regular"
// Avoid applying dynamic weight on custom font descriptors to prevent static let heading1 = resolvedFont(size: 28, fallbackWeight: .bold)
// SwiftUI runtime warnings on some platforms/toolchains. static let heading25 = resolvedFont(size: 25, fallbackWeight: .bold)
static let heading1 = Font.custom(fontFamily, size: 28) static let heading2 = resolvedFont(size: 20, fallbackWeight: .semibold)
static let heading25 = Font.custom(fontFamily, size: 25) static let heading3 = resolvedFont(size: 16, fallbackWeight: .semibold)
static let heading2 = Font.custom(fontFamily, size: 20) static let body = resolvedFont(size: 16, fallbackWeight: .regular)
static let heading3 = Font.custom(fontFamily, size: 16) static let button = resolvedFont(size: 14, fallbackWeight: .semibold)
static let body = Font.custom(fontFamily, size: 16) static let caption = resolvedFont(size: 10, fallbackWeight: .regular)
static let button = Font.custom(fontFamily, size: 14) static let overline = resolvedFont(size: 11, fallbackWeight: .regular)
static let caption = Font.custom(fontFamily, size: 10)
static let overline = Font.custom(fontFamily, size: 11)
static let bodyLineHeight: CGFloat = 1.6 static let bodyLineHeight: CGFloat = 1.6
static let buttonLetterSpacing: CGFloat = 0.08 static let buttonLetterSpacing: CGFloat = 0.08
static let captionLetterSpacing: CGFloat = 0.12 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 // MARK: - Layout
enum AppLayout { struct AppLayout {
private init() {}
static let radiusMD: CGFloat = 12 static let radiusMD: CGFloat = 12
static let radiusLG: CGFloat = 16 static let radiusLG: CGFloat = 16
static let radiusXL: CGFloat = 24 static let radiusXL: CGFloat = 24
@@ -68,7 +96,8 @@ struct ShadowSpec {
let y: CGFloat 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 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) 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) { init(hex: String, alpha: Double = 1.0) {
let cleaned = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) let cleaned = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0 var int: UInt64 = 0
Scanner(string: cleaned).scanHexInt64(&int) if let val = UInt64(cleaned, radix: 16) {
int = val
}
let r, g, b: UInt64 let r, g, b: UInt64
switch cleaned.count { switch cleaned.count {

View File

@@ -1,60 +1,30 @@
import Foundation import Foundation
import SkipFuse import OSLog
import SwiftUI import SwiftUI
/// A logger for the PediFoods module.
let logger: Logger = Logger(subsystem: "com.br.pedifoods.app", category: "PediFoods") 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. public struct PediFoodsRootView: View {
/// public init() {}
/// The default implementation merely loads the `ContentView` for the app and logs a message.
/* SKIP @bridge */public struct PediFoodsRootView : View {
/* SKIP @bridge */public init() {
}
public var body: some View { public var body: some View {
ContentView() 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. public final class PediFoodsAppDelegate: Sendable {
/// public static let shared = PediFoodsAppDelegate()
/// These functions can update a shared observable object to communicate app state changes to interested views.
/* SKIP @bridge */public final class PediFoodsAppDelegate : Sendable {
/* SKIP @bridge */public static let shared = PediFoodsAppDelegate()
private init() { private init() {}
}
/* SKIP @bridge */public func onInit() { public func onInit() { logger.debug("onInit") }
logger.debug("onInit") public func onLaunch() { logger.debug("onLaunch") }
} public func onResume() {
/* SKIP @bridge */public func onLaunch() {
logger.debug("onLaunch")
}
/* SKIP @bridge */public func onResume() {
logger.debug("onResume") logger.debug("onResume")
NotificationCenter.default.post(name: .appDidResume, object: nil) NotificationCenter.default.post(name: .appDidResume, object: nil)
} }
public func onPause() { logger.debug("onPause") }
/* SKIP @bridge */public func onPause() { public func onStop() { logger.debug("onStop") }
logger.debug("onPause") public func onDestroy() { logger.debug("onDestroy") }
} public func onLowMemory() { logger.debug("onLowMemory") }
/* SKIP @bridge */public func onStop() {
logger.debug("onStop")
}
/* SKIP @bridge */public func onDestroy() {
logger.debug("onDestroy")
}
/* SKIP @bridge */public func onLowMemory() {
logger.debug("onLowMemory")
}
} }

View File

@@ -297,6 +297,9 @@
"Cardápio indisponível no momento." : { "Cardápio indisponível no momento." : {
"comment" : "A message displayed when a store's menu is unavailable.", "comment" : "A message displayed when a store's menu is unavailable.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
},
"Carregando cardápio..." : {
}, },
"Carregando pedido..." : { "Carregando pedido..." : {

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

View File

@@ -14,8 +14,83 @@
], ],
"groups" : [ "groups" : [
{ {
"hidden" : false,
"layers" : [ "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" : { "shadow" : {
"kind" : "neutral", "kind" : "neutral",
@@ -27,54 +102,18 @@
} }
}, },
{ {
"layers" : [ "blend-mode" : "normal",
"hidden-specializations" : [
{ {
"blend-mode-specializations" : [ "value" : false
{ },
"value" : "normal" {
}, "idiom" : "iOS",
{ "value" : false
"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
]
}
}
]
} }
],
"layers" : [
], ],
"shadow" : { "shadow" : {
"kind" : "neutral", "kind" : "neutral",
@@ -87,6 +126,9 @@
} }
], ],
"supported-platforms" : { "supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : [ "squares" : [
"iOS" "iOS"
] ]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -152,4 +152,38 @@ struct ValidateDeliveryAddressResult: Decodable {
let deliveryFee: Double? let deliveryFee: Double?
let deliveryTime: String? let deliveryTime: String?
let sameCity: Bool? 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))
}
} }

View File

@@ -8,8 +8,6 @@ enum ReviewPlatform: String, Encodable {
static var current: ReviewPlatform { static var current: ReviewPlatform {
#if os(iOS) #if os(iOS)
return .ios return .ios
#elseif os(Android)
return .android
#else #else
return .web return .web
#endif #endif

View File

@@ -258,20 +258,16 @@ final class FeatureControlService {
} }
private func featureKeys() -> [String] { 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 let items = raw
.split(separator: ",") .split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { $0.isEmpty == false } .filter { $0.isEmpty == false }
return items.isEmpty ? ["fc.checkout_v2"] : items return items.isEmpty ? ["at.ios.only"] : items
} }
private func platformName() -> String { private func platformName() -> String {
#if os(Android)
return "android"
#else
return "ios" return "ios"
#endif
} }
private func appVersion() -> String { private func appVersion() -> String {

View File

@@ -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'

View File

@@ -62,6 +62,16 @@ struct FeatureFlagsState: Codable, Equatable {
var source: String = "default" var source: String = "default"
var values: [String: FeatureFlagValue] = [:] var values: [String: FeatureFlagValue] = [:]
var raw: [String: FeatureControlRawFlag] = [:] 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 { enum AppModal: String, Identifiable {

View File

@@ -1,26 +1,10 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
#if os(Android) extension Notification.Name {
@MainActor static let snackbarDidChange = Notification.Name("snackbarDidChange")
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) {}
} }
#else
@MainActor @MainActor
final class SnackbarCenter: ObservableObject { final class SnackbarCenter: ObservableObject {
static let shared = SnackbarCenter() static let shared = SnackbarCenter()
@@ -71,7 +55,6 @@ final class SnackbarCenter: ObservableObject {
} }
} }
} }
#endif
enum SnackbarStyle: Sendable { enum SnackbarStyle: Sendable {
case info case info

View File

@@ -11,26 +11,76 @@ import CoreGraphics
import UIKit import UIKit
#endif #endif
#if !canImport(UIKit) #if canImport(AppKit)
struct UIDevice { import AppKit
static let topNotch = 0.0 #endif
static let bottomNotch = 0.0
var modelName: String { #if canImport(UIKit)
"android" 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 #endif
#if !(canImport(LCEssentials) && os(iOS)) func appReadClipboardText() -> String? {
func printLog(title: String, msg: String) { #if canImport(UIKit)
print("[\(title)] \(msg)") return UIPasteboard.general.string
#elseif canImport(AppKit)
return NSPasteboard.general.string(forType: .string)
#else
return nil
#endif
} }
func printError(title: String, msg: String) { func appWriteClipboardText(_ value: String) {
print("[\(title)] \(msg)") #if canImport(UIKit)
} UIPasteboard.general.string = value
#elseif canImport(AppKit)
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(value, forType: .string)
#else
_ = value
#endif #endif
}
extension View { extension View {
@ViewBuilder @ViewBuilder
@@ -38,7 +88,9 @@ extension View {
#if os(macOS) #if os(macOS)
self self
#else #else
self.navigationBarTitleDisplayMode(.inline) self
.navigationBarTitleDisplayMode(.inline)
.modifier(AppRoundedBackButtonModifier())
#endif #endif
} }
@@ -72,32 +124,48 @@ extension View {
@ViewBuilder @ViewBuilder
func appContentShape<S: Shape>(_ shape: S) -> some View { func appContentShape<S: Shape>(_ shape: S) -> some View {
#if os(Android)
self
#else
self.contentShape(shape) self.contentShape(shape)
#endif
} }
@ViewBuilder @ViewBuilder
func appBottomSafeAreaInset<Content: View>(@ViewBuilder content: () -> Content) -> some View { func appBottomSafeAreaInset<Content: View>(@ViewBuilder content: () -> Content) -> some View {
#if os(Android)
self.overlay(alignment: .bottom) {
content()
}
#else
self.safeAreaInset(edge: .bottom) { self.safeAreaInset(edge: .bottom) {
content() content()
} }
#endif
} }
@ViewBuilder @ViewBuilder
func appLayoutPriority(_ value: Double) -> some View { func appLayoutPriority(_ value: Double) -> some View {
#if os(Android)
self
#else
self.layoutPriority(value) 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

View File

@@ -17,9 +17,7 @@ struct TermsOfUseView: View {
} }
.background((colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea()) .background((colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea())
.navigationTitle("Termos de Uso") .navigationTitle("Termos de Uso")
#if os(iOS)
.appInlineNavigationTitle() .appInlineNavigationTitle()
#endif
} }
} }
@@ -40,8 +38,6 @@ struct PrivacyPolicyView: View {
} }
.background((colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea()) .background((colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea())
.navigationTitle("Privacidade") .navigationTitle("Privacidade")
#if os(iOS)
.appInlineNavigationTitle() .appInlineNavigationTitle()
#endif
} }
} }

View File

@@ -14,13 +14,8 @@ struct LoginEmailView: View {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@ViewBuilder private var logoImage: some View { @ViewBuilder private var logoImage: some View {
#if os(Android)
SwiftUI.Image("pedifoods") SwiftUI.Image("pedifoods")
.resizable() .resizable()
#else
SwiftUI.Image("pedifoods")
.resizable()
#endif
} }
var body: some View { var body: some View {
@@ -40,9 +35,9 @@ struct LoginEmailView: View {
VStack(spacing: 16) { VStack(spacing: 16) {
VStack(alignment: .leading, 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 .onChange(of: phone) { _, newValue in
let masked = formatPhoneBR(newValue) let masked = formatPhoneBR(newValue)
if masked != newValue { if masked != newValue {
@@ -78,6 +73,7 @@ struct LoginEmailView: View {
dismiss() dismiss()
} }
} }
.buttonStyle(.plain)
.font(AppTypography.body) .font(AppTypography.body)
.padding(.top, 8) .padding(.top, 8)
@@ -125,6 +121,7 @@ struct LoginEmailView: View {
struct LoginField: View { struct LoginField: View {
let icon: String let icon: String
let placeholder: String let placeholder: String
let keyboardType: UIKeyboardType
@Binding var text: String @Binding var text: String
var body: some View { var body: some View {
@@ -135,6 +132,7 @@ struct LoginField: View {
TextField(text: $text, prompt: Text(placeholder).foregroundColor(Color.gray)) { } TextField(text: $text, prompt: Text(placeholder).foregroundColor(Color.gray)) { }
.appNoAutoCap() .appNoAutoCap()
.foregroundColor(.black) .foregroundColor(.black)
.keyboardType(keyboardType)
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
.frame(height: 52) .frame(height: 52)

View File

@@ -15,23 +15,13 @@ struct LoginView: View {
@State var lastAnimatedToken = 0 @State var lastAnimatedToken = 0
@ViewBuilder private var logoImage: some View { @ViewBuilder private var logoImage: some View {
#if os(Android)
SwiftUI.Image("pedifoods") SwiftUI.Image("pedifoods")
.resizable() .resizable()
#else
SwiftUI.Image("pedifoods")
.resizable()
#endif
} }
@ViewBuilder private var pinHeroImage: some View { @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") SwiftUI.Image("pin_image_app")
.resizable() .resizable()
#endif
} }
var body: some View { var body: some View {
@@ -82,6 +72,7 @@ struct LoginView: View {
.tint(AppColors.tertiary) .tint(AppColors.tertiary)
.offset(y: buttonVisible ? 0 : 140) .offset(y: buttonVisible ? 0 : 140)
.opacity(buttonVisible ? 1.0 : 0.0) .opacity(buttonVisible ? 1.0 : 0.0)
.buttonStyle(.plain)
HStack(spacing: 6) { HStack(spacing: 6) {
Text("Não tem conta ainda?") Text("Não tem conta ainda?")
@@ -89,6 +80,7 @@ struct LoginView: View {
Button("Criar conta") { Button("Criar conta") {
navigate(.registration) navigate(.registration)
} }
.buttonStyle(.plain)
.foregroundStyle(AppColors.primary) .foregroundStyle(AppColors.primary)
} }
.font(AppTypography.body) .font(AppTypography.body)

View File

@@ -64,6 +64,10 @@ struct OtpView: View {
if trimmed != newValue { if trimmed != newValue {
otp = trimmed otp = trimmed
} }
if trimmed.count == 8 && !isLoading {
isOtpFocused = false
validateOtp()
}
} }
} }
.frame(height: 204) .frame(height: 204)
@@ -84,6 +88,7 @@ struct OtpView: View {
Button(resendButtonTitle) { Button(resendButtonTitle) {
resendOtp() resendOtp()
} }
.buttonStyle(.plain)
.font(AppTypography.heading3) .font(AppTypography.heading3)
.foregroundStyle(AppColors.primary) .foregroundStyle(AppColors.primary)
.disabled(isResending || !canResend) .disabled(isResending || !canResend)
@@ -93,6 +98,7 @@ struct OtpView: View {
Button("Colar código") { Button("Colar código") {
pasteOtpFromClipboard() pasteOtpFromClipboard()
} }
.buttonStyle(.plain)
.font(AppTypography.heading3) .font(AppTypography.heading3)
.foregroundStyle(AppColors.primary) .foregroundStyle(AppColors.primary)
.padding(.top, 22) .padding(.top, 22)
@@ -116,6 +122,7 @@ struct OtpView: View {
.padding(.bottom, 18) .padding(.bottom, 18)
} }
} }
.scrollDismissesKeyboard(.interactively)
.background(AppColors.backgroundLight) .background(AppColors.backgroundLight)
.ignoresSafeArea() .ignoresSafeArea()
.onAppear { .onAppear {
@@ -181,31 +188,20 @@ struct OtpView: View {
} }
private func pasteOtpFromClipboard() { private func pasteOtpFromClipboard() {
#if canImport(UIKit) let raw = appReadClipboardText() ?? ""
let raw = UIPasteboard.general.string ?? ""
let digits = raw.filter(\.isNumber) let digits = raw.filter(\.isNumber)
let trimmed = String(digits.prefix(8)) let trimmed = String(digits.prefix(8))
if trimmed.isEmpty == false { if trimmed.isEmpty == false {
otp = trimmed 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() { private func autoFillOtpFromClipboardIfAvailable() {
#if canImport(UIKit)
guard otp.isEmpty else { return } guard otp.isEmpty else { return }
let raw = UIPasteboard.general.string ?? "" let raw = appReadClipboardText() ?? ""
let digits = raw.filter(\.isNumber) let digits = raw.filter(\.isNumber)
guard digits.count >= 8 else { return } guard digits.count >= 8 else { return }
otp = String(digits.prefix(8)) otp = String(digits.prefix(8))
#endif
} }
private var otpDeliveryMessage: String { private var otpDeliveryMessage: String {

View File

@@ -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 } 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 { @ViewBuilder private var logoImage: some View {
#if os(Android)
SwiftUI.Image("pedifoods") SwiftUI.Image("pedifoods")
.resizable() .resizable()
#else
SwiftUI.Image("pedifoods")
.resizable()
#endif
} }
var body: some View { var body: some View {
@@ -49,9 +44,9 @@ struct RegistrationView: View {
VStack(spacing: 16) { VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
LoginField(icon: "person", placeholder: "Ex: Maria Silva", text: $name) LoginField(icon: "person", placeholder: "Ex: Maria Silva", keyboardType: .default, text: $name)
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: .namePhonePad, text: $phone)
.onChange(of: phone) { _, newValue in .onChange(of: phone) { _, newValue in
let masked = formatPhoneBR(newValue) let masked = formatPhoneBR(newValue)
if masked != newValue { if masked != newValue {

View File

@@ -36,12 +36,7 @@ struct LaunchSplashView: View {
} }
@ViewBuilder var splashLogo: some View { @ViewBuilder var splashLogo: some View {
#if os(Android)
SwiftUI.Image("pedifoods") SwiftUI.Image("pedifoods")
.resizable() .resizable()
#else
SwiftUI.Image("pedifoods")
.resizable()
#endif
} }
} }

View File

@@ -32,13 +32,8 @@ struct AddAddressFormView: View {
} }
@ViewBuilder private var logoImage: some View { @ViewBuilder private var logoImage: some View {
#if os(Android)
SwiftUI.Image("pedifoods") SwiftUI.Image("pedifoods")
.resizable() .resizable()
#else
SwiftUI.Image("pedifoods")
.resizable()
#endif
} }
var body: some View { var body: some View {
@@ -47,6 +42,13 @@ struct AddAddressFormView: View {
ScrollView { ScrollView {
VStack(spacing: 0) { VStack(spacing: 0) {
screenHeader(
title: existingAddress == nil ? "Novo endereço" : "Editar endereço",
onBack: { dismiss() }
)
.padding(.horizontal, 24)
.padding(.bottom, 12)
logoImage logoImage
.scaledToFit() .scaledToFit()
.frame(width: 120, height: 120) .frame(width: 120, height: 120)
@@ -63,8 +65,8 @@ struct AddAddressFormView: View {
.padding(.bottom, 20) .padding(.bottom, 20)
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
LoginField(icon: "tag", placeholder: "Ex: Casa, Trabalho", text: $label) LoginField(icon: "tag", placeholder: "Ex: Casa, Trabalho", keyboardType: .default, text: $label)
LoginField(icon: "mail", placeholder: "CEP", text: $zipCode) LoginField(icon: "mail", placeholder: "CEP", keyboardType: .numberPad, text: $zipCode)
.onChange(of: zipCode) { _, newValue in .onChange(of: zipCode) { _, newValue in
let masked = formatZipCodeBR(newValue) let masked = formatZipCodeBR(newValue)
if masked != newValue { if masked != newValue {
@@ -92,12 +94,12 @@ struct AddAddressFormView: View {
.foregroundStyle(AppColors.textMuted) .foregroundStyle(AppColors.textMuted)
.padding(.horizontal, 6) .padding(.horizontal, 6)
} }
LoginField(icon: "mappin.and.ellipse", placeholder: "Rua / Avenida", text: $address) LoginField(icon: "mappin.and.ellipse", placeholder: "Rua / Avenida", keyboardType: .default, text: $address)
LoginField(icon: "number", placeholder: "Número", text: $number) LoginField(icon: "number", placeholder: "Número", keyboardType: .default, text: $number)
LoginField(icon: "plus.app", placeholder: "Complemento (opcional)", text: $complement) LoginField(icon: "plus.app", placeholder: "Complemento (opcional)", keyboardType: .default, text: $complement)
LoginField(icon: "square.grid.2x2", placeholder: "Bairro", text: $neighborhood) LoginField(icon: "square.grid.2x2", placeholder: "Bairro", keyboardType: .default, text: $neighborhood)
LoginField(icon: "building.2", placeholder: "Cidade", text: $city) LoginField(icon: "building.2", placeholder: "Cidade", keyboardType: .default, text: $city)
LoginField(icon: "map", placeholder: "Estado (UF)", text: $state) LoginField(icon: "map", placeholder: "Estado (UF)", keyboardType: .default, text: $state)
.onChange(of: state) { _, newValue in .onChange(of: state) { _, newValue in
let normalized = String(newValue.uppercased().prefix(2)) let normalized = String(newValue.uppercased().prefix(2))
if normalized != newValue { 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() { private func saveAddress() {
guard !isLoading else { return } guard !isLoading else { return }
let latLong: [Double]? = { let latLong: [Double]? = {

View File

@@ -22,7 +22,7 @@ struct AddressesView: View {
ScrollView(showsIndicators: false) { ScrollView(showsIndicators: false) {
VStack(spacing: 20) { VStack(spacing: 20) {
header screenHeader(title: "Meus Endereços", onBack: { dismiss() })
if let message { if let message {
Text(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 { var bottomOverlay: some View {
ZStack(alignment: .bottom) { ZStack(alignment: .bottom) {
Rectangle() 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) { private func selectAddress(_ address: CustomerAddress) {
appState.address.selectedId = address.id appState.address.selectedId = address.id
let label = (address.label ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let label = (address.label ?? "").trimmingCharacters(in: .whitespacesAndNewlines)

View File

@@ -692,12 +692,7 @@ struct PaymentPixView: View {
} }
private func copyToClipboard(_ value: String) { private func copyToClipboard(_ value: String) {
#if canImport(UIKit) appWriteClipboardText(value)
UIPasteboard.general.string = value
#elseif canImport(AppKit)
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(value, forType: .string)
#endif
} }
private var parsedExpirationDate: Date? { private var parsedExpirationDate: Date? {

View File

@@ -4,7 +4,6 @@ enum HomeScrollCoordinateSpace {
static let name = "home-scroll" static let name = "home-scroll"
} }
#if os(iOS)
struct HomeScrollOffsetPreferenceKey: PreferenceKey { struct HomeScrollOffsetPreferenceKey: PreferenceKey {
static let defaultValue: CGFloat = 0 static let defaultValue: CGFloat = 0
@@ -32,4 +31,3 @@ struct ScrollOffsetObserver: View {
} }
} }
} }
#endif

View File

@@ -61,6 +61,7 @@ extension HomeView {
rating: store.rating ?? 0, rating: store.rating ?? 0,
reviews: String(store.positiveReviews ?? store.reviewsCount ?? 0), reviews: String(store.positiveReviews ?? store.reviewsCount ?? 0),
distance: formatDistance(store.distance), distance: formatDistance(store.distance),
deliveryFee: store.deliveryFee,
category: store.category ?? "Loja", category: store.category ?? "Loja",
promoText: nil, promoText: nil,
isFavorite: appState.favorites.storeIds.contains(store.id), isFavorite: appState.favorites.storeIds.contains(store.id),

View File

@@ -48,7 +48,7 @@ struct HomeView: View {
refreshCategories: true refreshCategories: true
) )
} }
.coordinateSpace(name: HomeScrollCoordinateSpace.name) .appNamedCoordinateSpace(HomeScrollCoordinateSpace.name)
header(collapseProgress: collapseProgress, height: headerHeight) header(collapseProgress: collapseProgress, height: headerHeight)
.frame(maxWidth: .infinity, alignment: .top) .frame(maxWidth: .infinity, alignment: .top)
@@ -101,6 +101,7 @@ struct HomeView: View {
storeCategory: store.category, storeCategory: store.category,
storeRating: store.rating, storeRating: store.rating,
storeDistance: store.distance, storeDistance: store.distance,
storeDeliveryFee: store.deliveryFee,
appState: $appState appState: $appState
) )
} label: { } label: {
@@ -121,15 +122,17 @@ struct HomeView: View {
} }
} }
section(title: "#PediPromo") { if appState.featureFlags.isEnabled("at.promo") {
ScrollView(.horizontal, showsIndicators: false) { section(title: "#PediPromo") {
HStack(spacing: 16) { ScrollView(.horizontal, showsIndicators: false) {
ForEach(specials) { item in HStack(spacing: 16) {
SpecialOfferCard(model: item) ForEach(specials) { item in
.frame(width: 260, height: 120) 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) .font(AppTypography.heading3)
.foregroundStyle(AppColors.primary) .foregroundStyle(AppColors.primary)
} }
@@ -177,6 +181,7 @@ struct HomeView: View {
storeCategory: store.category, storeCategory: store.category,
storeRating: store.rating, storeRating: store.rating,
storeDistance: store.distance, storeDistance: store.distance,
storeDeliveryFee: store.deliveryFee,
appState: $appState appState: $appState
) )
} label: { } label: {
@@ -204,7 +209,7 @@ struct HomeView: View {
return ZStack(alignment: .top) { return ZStack(alignment: .top) {
RoundedRectangle(cornerRadius: 32, style: .continuous) RoundedRectangle(cornerRadius: 32, style: .continuous)
.fill(AppColors.brandDark) .fill(AppColors.primary)
.frame(height: height) .frame(height: height)
.overlay(headerRings.opacity(1 - collapseProgress), alignment: .topTrailing) .overlay(headerRings.opacity(1 - collapseProgress), alignment: .topTrailing)
@@ -449,16 +454,12 @@ struct HomeView: View {
@ViewBuilder @ViewBuilder
private var scrollOffsetObserver: some View { private var scrollOffsetObserver: some View {
#if os(iOS)
ScrollOffsetObserver { y in ScrollOffsetObserver { y in
// Use only upward displacement for collapse and ignore top bounce. // Use only upward displacement for collapse and ignore top bounce.
let normalized = max(0, y) let normalized = max(0, y)
scrollOffset = normalized scrollOffset = normalized
} }
.frame(width: 0, height: 0) .frame(width: 0, height: 0)
#else
EmptyView()
#endif
} }
private var addressCacheScope: String { private var addressCacheScope: String {

View File

@@ -28,7 +28,7 @@ struct CategoryChip: View {
.foregroundStyle(isActive ? AppColors.textInverse : AppColors.textPrimary) .foregroundStyle(isActive ? AppColors.textInverse : AppColors.textPrimary)
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.vertical, 10) .padding(.vertical, 10)
.background(isActive ? AppColors.brandDark : AppColors.surface) .background(isActive ? AppColors.primary : AppColors.surface)
.clipShape(Capsule()) .clipShape(Capsule())
} }
} }
@@ -49,6 +49,7 @@ struct SearchBar: View {
Image(systemName: "slider.horizontal.3") Image(systemName: "slider.horizontal.3")
.foregroundStyle(AppColors.textMuted) .foregroundStyle(AppColors.textMuted)
} }
.buttonStyle(.plain)
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
.frame(height: 52) .frame(height: 52)

View File

@@ -72,12 +72,12 @@ struct MainTabView: View {
.font(AppTypography.heading3) .font(AppTypography.heading3)
} }
} }
.foregroundStyle(isActive ? AppColors.primary : AppColors.textPrimary) .foregroundStyle(isActive ? AppColors.textInverse : AppColors.textPrimary)
.padding(.horizontal, 18) .padding(.horizontal, 18)
.padding(.vertical, 10) .padding(.vertical, 10)
.background( .background(
Capsule() Capsule()
.fill(isActive ? AppColors.brandSoft : Color.clear) .fill(isActive ? AppColors.primary : Color.clear)
) )
} }
.buttonStyle(.plain) .buttonStyle(.plain)

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