Merge pull request 'feature/step-001' (#4) from feature/step-001 into main
Reviewed-on: Loverde-Company-LTDA/Pedi-Foods-Skip#4
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 82 KiB |
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
BIN
pedi-foods/Android/app/src/main/res/drawable/login_dark.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
pedi-foods/Android/app/src/main/res/drawable/login_light.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
pedi-foods/Android/app/src/main/res/drawable/pedifoods.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
pedi-foods/Android/app/src/main/res/drawable/pedifoods_dark.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
pedi-foods/Android/app/src/main/res/drawable/pin_image_app.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 406 KiB |
|
After Width: | Height: | Size: 924 KiB |
|
After Width: | Height: | Size: 951 KiB |
|
After Width: | Height: | Size: 940 KiB |
|
After Width: | Height: | Size: 926 KiB |
BIN
pedi-foods/Android/app/src/main/res/drawable/tracking_ready.png
Normal file
|
After Width: | Height: | Size: 799 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 21 KiB |
12
pedi-foods/Android/app/src/main/res/values-night/themes.xml
Normal 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>
|
||||
5
pedi-foods/Android/app/src/main/res/values/colors.xml
Normal 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>
|
||||
12
pedi-foods/Android/app/src/main/res/values/themes.xml
Normal 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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 82 KiB |
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// swift-tools-version: 6.1
|
||||
// This is a Skip (https://skip.tools) package.
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
@@ -10,15 +9,12 @@ let package = Package(
|
||||
.library(name: "PediFoods", type: .dynamic, targets: ["PediFoods"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://source.skip.tools/skip.git", from: "1.8.4"),
|
||||
.package(path: "Vendor/skip-fuse-ui"),
|
||||
.package(url: "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials.git", from: "1.0.5")
|
||||
],
|
||||
targets: [
|
||||
.target(name: "PediFoods", dependencies: [
|
||||
.product(name: "SkipFuseUI", package: "skip-fuse-ui"),
|
||||
.product(name: "LCEssentials", package: "lcessentials", condition: .when(platforms: [.iOS]))
|
||||
], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]),
|
||||
.product(name: "LCEssentials", package: "lcessentials")
|
||||
], resources: [.process("Resources")]),
|
||||
.testTarget(name: "PediFoodsTests", dependencies: ["PediFoods"]),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,60 +1,30 @@
|
||||
import Foundation
|
||||
import SkipFuse
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
/// A logger for the PediFoods module.
|
||||
let logger: Logger = Logger(subsystem: "com.br.pedifoods.app", category: "PediFoods")
|
||||
|
||||
/// The shared top-level view for the app, loaded from the platform-specific App delegates below.
|
||||
///
|
||||
/// The default implementation merely loads the `ContentView` for the app and logs a message.
|
||||
/* SKIP @bridge */public struct PediFoodsRootView : View {
|
||||
/* SKIP @bridge */public init() {
|
||||
}
|
||||
public struct PediFoodsRootView: View {
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
ContentView()
|
||||
.task {
|
||||
logger.info("Skip app logs are viewable in the Xcode console for iOS; Android logs can be viewed in Studio or using adb logcat")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Global application delegate functions.
|
||||
///
|
||||
/// These functions can update a shared observable object to communicate app state changes to interested views.
|
||||
/* SKIP @bridge */public final class PediFoodsAppDelegate : Sendable {
|
||||
/* SKIP @bridge */public static let shared = PediFoodsAppDelegate()
|
||||
public final class PediFoodsAppDelegate: Sendable {
|
||||
public static let shared = PediFoodsAppDelegate()
|
||||
|
||||
private init() {
|
||||
}
|
||||
private init() {}
|
||||
|
||||
/* SKIP @bridge */public func onInit() {
|
||||
logger.debug("onInit")
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onLaunch() {
|
||||
logger.debug("onLaunch")
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onResume() {
|
||||
public func onInit() { logger.debug("onInit") }
|
||||
public func onLaunch() { logger.debug("onLaunch") }
|
||||
public func onResume() {
|
||||
logger.debug("onResume")
|
||||
NotificationCenter.default.post(name: .appDidResume, object: nil)
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onPause() {
|
||||
logger.debug("onPause")
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onStop() {
|
||||
logger.debug("onStop")
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onDestroy() {
|
||||
logger.debug("onDestroy")
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onLowMemory() {
|
||||
logger.debug("onLowMemory")
|
||||
}
|
||||
public func onPause() { logger.debug("onPause") }
|
||||
public func onStop() { logger.debug("onStop") }
|
||||
public func onDestroy() { logger.debug("onDestroy") }
|
||||
public func onLowMemory() { logger.debug("onLowMemory") }
|
||||
}
|
||||
|
||||
@@ -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..." : {
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 120 KiB |
@@ -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"
|
||||
]
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 82 KiB |
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]? = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||