master merged

This commit is contained in:
Daniel Arantes Loverde
2026-06-03 14:52:27 -03:00
parent 980a106661
commit f51319b486
112 changed files with 1051 additions and 1139 deletions

View File

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

View File

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

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`
- `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 <JWT>`, 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

View File

@@ -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);

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

View File

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

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",
"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
}
}

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>"; };
496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = PediFoods.xcconfig; sourceTree = "<group>"; };
496EB72F2A6AE4DE00C1253C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
499AB9082B0581F4005E8330 /* plugins */ = {isa = PBXFileReference; lastKnownFileType = folder; name = plugins; path = ../../Intermediates.noindex/BuildToolPluginIntermediates; sourceTree = BUILT_PRODUCTS_DIR; };
49F90C2B2A52156200F06D93 /* Main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Main.swift; path = Sources/Main.swift; sourceTree = SOURCE_ROOT; };
49F90C2F2A52156300F06D93 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
49F90C312A52156300F06D93 /* Entitlements.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Entitlements.plist; sourceTree = "<group>"; };
@@ -66,14 +65,6 @@
name = Products;
sourceTree = "<group>";
};
49AB54462B066A7E007B79B2 /* SkipStone */ = {
isa = PBXGroup;
children = (
499AB9082B0581F4005E8330 /* plugins */,
);
name = SkipStone;
sourceTree = "<group>";
};
49F90C1F2A52156200F06D93 = {
isa = PBXGroup;
children = (
@@ -83,7 +74,6 @@
496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */,
493609562A6B7EAE00C401E2 /* PediFoods */,
49F90C2A2A52156200F06D93 /* App */,
49AB54462B066A7E007B79B2 /* SkipStone */,
496BDBEC2B89A47800C09264 /* Products */,
);
sourceTree = "<group>";
@@ -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 */

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
// swift-tools-version: 6.1
// This is a Skip (https://skip.tools) package.
import PackageDescription
let package = Package(
@@ -10,13 +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(url: "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials.git", from: "1.0.5")
],
targets: [
.target(name: "PediFoods", dependencies: [
.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"]),
]
)

View File

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

View File

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

View File

@@ -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)
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,59 +2,29 @@ import Foundation
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") }
}

View File

@@ -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..." : {

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" : [
{
"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"
]

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 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))
}
}

View File

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

View File

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

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 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 {

View File

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

View File

@@ -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<S: Shape>(_ shape: S) -> some View {
#if os(Android)
self
#else
self.contentShape(shape)
#endif
}
@ViewBuilder
func appBottomSafeAreaInset<Content: View>(@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

View File

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

View File

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

View File

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

View File

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

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 }
@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 {

View File

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

View File

@@ -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]? = {

View File

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

View File

@@ -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? {

View File

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

View File

@@ -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),

View File

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

View File

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

View File

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

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