[public-store-distance] Consume the public distance contract, stop labelling it as unavailable

Guest browsing showed 'Distância indisponível' on every store card and a
'--' tile on store detail, because the public locator response carries no
distance and StoreSummary.init(publicItem:) hardcoded nil.

Backend contract (docs/plans/public-store-distance-consumer.md): distance
is a number, never null, 0 means unavailable. Implemented app-side ahead of
the backend - every change is forward-compatible, and the part that removes
the broken label works with no backend at all.

- PublicStoreListItem / PublicStoreDetail: + distance (optional, so today's
  responses without the field still decode).
- StoreSummary.init(publicItem:): normalize the 0 sentinel to nil, so one
  representation of 'unknown' reaches the label and the max-distance filter.
- fetchStoreDetail: send state/city from GuestLocationStore by default -
  the server needs them to resolve the city centroid.
- formatDistance: empty string for nil/0/negative. It previously returned
  'Distância indisponível' for nil, and - found by the new test - '0 m' for
  0 and '-1000 m' for a negative.
- StoreCard: drop the distance segment and its '·' separator together,
  otherwise the row ended in a dangling separator.
- StoreDetailView: drop the DISTÂNCIA tile and its divider instead of '--'.

Also fixes the same label in the authenticated flow when the user declined
location and has no address coordinates.

Tests: PublicStoreDistanceTests (decode with/without the field, passthrough,
0 normalization) + the empty case in HomeViewFilteringTests.
This commit is contained in:
Daniel Arantes Loverde
2026-08-27 16:33:01 -03:00
parent 0f0672f18d
commit a0e4375914
10 changed files with 154 additions and 33 deletions

View File

@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 56; objectVersion = 54;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -79,9 +79,10 @@
7CC171B83BC4CDEC38B25F8C /* SnackbarCenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2CE116C585A7529CF309B3D /* SnackbarCenterTests.swift */; }; 7CC171B83BC4CDEC38B25F8C /* SnackbarCenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2CE116C585A7529CF309B3D /* SnackbarCenterTests.swift */; };
7D7BF04765D6CA6AC4F11DDD /* StoreCatalogNormalizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18EC38EEE1E7105BC21E354C /* StoreCatalogNormalizerTests.swift */; }; 7D7BF04765D6CA6AC4F11DDD /* StoreCatalogNormalizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18EC38EEE1E7105BC21E354C /* StoreCatalogNormalizerTests.swift */; };
80B476CCE2B90BEE2FA4CB37 /* PediFoodsUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E3E6C1834786CA3621B7BB /* PediFoodsUITestsLaunchTests.swift */; }; 80B476CCE2B90BEE2FA4CB37 /* PediFoodsUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E3E6C1834786CA3621B7BB /* PediFoodsUITestsLaunchTests.swift */; };
811217F88AA350A70CC22479 /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C724E02EC34BB1D4352C2FF0 /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 811217F88AA350A70CC22479 /* NotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C724E02EC34BB1D4352C2FF0 /* NotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
85EA52253BABA327EC07CFF0 /* AuthFormattersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561E00C486D60EC2DF720EAF /* AuthFormattersTests.swift */; }; 85EA52253BABA327EC07CFF0 /* AuthFormattersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561E00C486D60EC2DF720EAF /* AuthFormattersTests.swift */; };
8D6622AE9325D02B830BD115 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6CAA9B253B1A6C8815A0E69D /* LaunchScreen.storyboard */; }; 8D6622AE9325D02B830BD115 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6CAA9B253B1A6C8815A0E69D /* LaunchScreen.storyboard */; };
901A6A5A00C18FC59F764B85 /* PublicStoreDistanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E55893DD92CBFB24F212454 /* PublicStoreDistanceTests.swift */; };
90ADA376585F5438F3A1A1D6 /* HomeView+Favorites.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1517B141263438E466452037 /* HomeView+Favorites.swift */; }; 90ADA376585F5438F3A1A1D6 /* HomeView+Favorites.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1517B141263438E466452037 /* HomeView+Favorites.swift */; };
918BCBC1EAD7EE134DB9742D /* CheckoutTypesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FF15CEE5B137074E6B2489 /* CheckoutTypesTests.swift */; }; 918BCBC1EAD7EE134DB9742D /* CheckoutTypesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FF15CEE5B137074E6B2489 /* CheckoutTypesTests.swift */; };
926E0C4DD3C82485D4262A62 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B2F3558C58CD1E71F0F3A0 /* ContentView.swift */; }; 926E0C4DD3C82485D4262A62 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B2F3558C58CD1E71F0F3A0 /* ContentView.swift */; };
@@ -168,7 +169,7 @@
dstPath = ""; dstPath = "";
dstSubfolderSpec = 13; dstSubfolderSpec = 13;
files = ( files = (
811217F88AA350A70CC22479 /* NotificationService.appex in Embed Foundation Extensions */, 811217F88AA350A70CC22479 /* NotificationServiceExtension.appex in Embed Foundation Extensions */,
); );
name = "Embed Foundation Extensions"; name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -210,6 +211,7 @@
4C415019CC1F314EE7318864 /* ApiCardModelsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCardModelsTests.swift; sourceTree = "<group>"; }; 4C415019CC1F314EE7318864 /* ApiCardModelsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCardModelsTests.swift; sourceTree = "<group>"; };
4DC5416AEBA77C43CF55ABA8 /* PublicLocationPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicLocationPickerView.swift; sourceTree = "<group>"; }; 4DC5416AEBA77C43CF55ABA8 /* PublicLocationPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicLocationPickerView.swift; sourceTree = "<group>"; };
4E0CCEF22D356F9EC723999D /* HomeView+Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeView+Data.swift"; sourceTree = "<group>"; }; 4E0CCEF22D356F9EC723999D /* HomeView+Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeView+Data.swift"; sourceTree = "<group>"; };
4E55893DD92CBFB24F212454 /* PublicStoreDistanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicStoreDistanceTests.swift; sourceTree = "<group>"; };
507E868458359BE0E4FF25F1 /* ApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiService.swift; sourceTree = "<group>"; }; 507E868458359BE0E4FF25F1 /* ApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiService.swift; sourceTree = "<group>"; };
52BFF93F21509F1AD4F6676B /* PediFoodsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PediFoodsApp.swift; sourceTree = "<group>"; }; 52BFF93F21509F1AD4F6676B /* PediFoodsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PediFoodsApp.swift; sourceTree = "<group>"; };
54795EDED987426386226358 /* GuestLocationStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestLocationStoreTests.swift; sourceTree = "<group>"; }; 54795EDED987426386226358 /* GuestLocationStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestLocationStoreTests.swift; sourceTree = "<group>"; };
@@ -228,7 +230,7 @@
69A9FB3DE679C3D4C60E7E42 /* ApiConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiConfig.swift; sourceTree = "<group>"; }; 69A9FB3DE679C3D4C60E7E42 /* ApiConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiConfig.swift; sourceTree = "<group>"; };
6BF467DF1447525F04B242B0 /* ReviewsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsView.swift; sourceTree = "<group>"; }; 6BF467DF1447525F04B242B0 /* ReviewsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsView.swift; sourceTree = "<group>"; };
6CAA9B253B1A6C8815A0E69D /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; }; 6CAA9B253B1A6C8815A0E69D /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
6DD1D8390E57439A347F9E07 /* PediFoodsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PediFoodsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 6DD1D8390E57439A347F9E07 /* PediFoodsTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = PediFoodsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
6EC381D02A37312BE360024F /* Buttons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buttons.swift; sourceTree = "<group>"; }; 6EC381D02A37312BE360024F /* Buttons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buttons.swift; sourceTree = "<group>"; };
6FC454261845C1EDF970552A /* CheckoutTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckoutTypes.swift; sourceTree = "<group>"; }; 6FC454261845C1EDF970552A /* CheckoutTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckoutTypes.swift; sourceTree = "<group>"; };
7034E89267D9F5D86BF1E424 /* CartCheckoutFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartCheckoutFlowTests.swift; sourceTree = "<group>"; }; 7034E89267D9F5D86BF1E424 /* CartCheckoutFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartCheckoutFlowTests.swift; sourceTree = "<group>"; };
@@ -241,7 +243,7 @@
76B24E69A3F6A3EB86449870 /* AddAddressFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAddressFormView.swift; sourceTree = "<group>"; }; 76B24E69A3F6A3EB86449870 /* AddAddressFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAddressFormView.swift; sourceTree = "<group>"; };
794249487F718BCFBF176E8D /* ApiModelsDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiModelsDecodingTests.swift; sourceTree = "<group>"; }; 794249487F718BCFBF176E8D /* ApiModelsDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiModelsDecodingTests.swift; sourceTree = "<group>"; };
79D2854B8062D96742CFDD99 /* HomeViewComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewComponents.swift; sourceTree = "<group>"; }; 79D2854B8062D96742CFDD99 /* HomeViewComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewComponents.swift; sourceTree = "<group>"; };
7A6D74FD2D2F2F79349EA04E /* PediFoods.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PediFoods.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7A6D74FD2D2F2F79349EA04E /* PediFoods.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = PediFoods.app; sourceTree = BUILT_PRODUCTS_DIR; };
7A9F14B1BDD7747C546DFAF0 /* GuestSessionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestSessionService.swift; sourceTree = "<group>"; }; 7A9F14B1BDD7747C546DFAF0 /* GuestSessionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestSessionService.swift; sourceTree = "<group>"; };
7C13751F450A42A2BDB0783D /* ProfileLoggedOutFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileLoggedOutFlowTests.swift; sourceTree = "<group>"; }; 7C13751F450A42A2BDB0783D /* ProfileLoggedOutFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileLoggedOutFlowTests.swift; sourceTree = "<group>"; };
7CD06F9A7AF9365D1FBE35D0 /* AddressComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressComponents.swift; sourceTree = "<group>"; }; 7CD06F9A7AF9365D1FBE35D0 /* AddressComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressComponents.swift; sourceTree = "<group>"; };
@@ -250,7 +252,7 @@
89B662E8B7E1B8F1CF7E657E /* KeychainStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStore.swift; sourceTree = "<group>"; }; 89B662E8B7E1B8F1CF7E657E /* KeychainStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStore.swift; sourceTree = "<group>"; };
8A3FB85A2A2E2952B9A2BAB0 /* DeepLinkDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkDestination.swift; sourceTree = "<group>"; }; 8A3FB85A2A2E2952B9A2BAB0 /* DeepLinkDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkDestination.swift; sourceTree = "<group>"; };
8AA914FB8B32B106911FD67B /* LegalDocumentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalDocumentTests.swift; sourceTree = "<group>"; }; 8AA914FB8B32B106911FD67B /* LegalDocumentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalDocumentTests.swift; sourceTree = "<group>"; };
8AF987AB319512C123DFD558 /* PediFoodsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PediFoodsUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 8AF987AB319512C123DFD558 /* PediFoodsUITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = PediFoodsUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
8D2C6A0E85E133F947293181 /* OrderDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderDetailsView.swift; sourceTree = "<group>"; }; 8D2C6A0E85E133F947293181 /* OrderDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderDetailsView.swift; sourceTree = "<group>"; };
8DD2B73D89E6D33643EF58F5 /* DeepLinkRouteEffectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkRouteEffectTests.swift; sourceTree = "<group>"; }; 8DD2B73D89E6D33643EF58F5 /* DeepLinkRouteEffectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkRouteEffectTests.swift; sourceTree = "<group>"; };
8E3A0A2E253ABFCA35D02F28 /* TextFieldCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldCompat.swift; sourceTree = "<group>"; }; 8E3A0A2E253ABFCA35D02F28 /* TextFieldCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldCompat.swift; sourceTree = "<group>"; };
@@ -285,7 +287,7 @@
C44662BAA20AD862BE1DBC40 /* PublicLocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicLocationService.swift; sourceTree = "<group>"; }; C44662BAA20AD862BE1DBC40 /* PublicLocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicLocationService.swift; sourceTree = "<group>"; };
C4745C12F14B695C77DFE178 /* SnackbarOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnackbarOverlay.swift; sourceTree = "<group>"; }; C4745C12F14B695C77DFE178 /* SnackbarOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnackbarOverlay.swift; sourceTree = "<group>"; };
C6B08901A67C8D90A3B50882 /* ApiCustomerPayloadModelsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCustomerPayloadModelsTests.swift; sourceTree = "<group>"; }; C6B08901A67C8D90A3B50882 /* ApiCustomerPayloadModelsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCustomerPayloadModelsTests.swift; sourceTree = "<group>"; };
C724E02EC34BB1D4352C2FF0 /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; C724E02EC34BB1D4352C2FF0 /* NotificationServiceExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = NotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
CA44F5384D21B7F718F95522 /* StoreDetailSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreDetailSupport.swift; sourceTree = "<group>"; }; CA44F5384D21B7F718F95522 /* StoreDetailSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreDetailSupport.swift; sourceTree = "<group>"; };
CAB9F980426D39E93E091051 /* LocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationService.swift; sourceTree = "<group>"; }; CAB9F980426D39E93E091051 /* LocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationService.swift; sourceTree = "<group>"; };
CD5EE81AE4347B4086684F8B /* CartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartView.swift; sourceTree = "<group>"; }; CD5EE81AE4347B4086684F8B /* CartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartView.swift; sourceTree = "<group>"; };
@@ -344,7 +346,7 @@
126A579C448C784867DC3AB1 /* Products */ = { 126A579C448C784867DC3AB1 /* Products */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C724E02EC34BB1D4352C2FF0 /* NotificationService.appex */, C724E02EC34BB1D4352C2FF0 /* NotificationServiceExtension.appex */,
7A6D74FD2D2F2F79349EA04E /* PediFoods.app */, 7A6D74FD2D2F2F79349EA04E /* PediFoods.app */,
6DD1D8390E57439A347F9E07 /* PediFoodsTests.xctest */, 6DD1D8390E57439A347F9E07 /* PediFoodsTests.xctest */,
8AF987AB319512C123DFD558 /* PediFoodsUITests.xctest */, 8AF987AB319512C123DFD558 /* PediFoodsUITests.xctest */,
@@ -446,6 +448,7 @@
23B6B814027518969EB04341 /* HomeViewFilteringTests.swift */, 23B6B814027518969EB04341 /* HomeViewFilteringTests.swift */,
5A82497C63D507E82FE02693 /* ImageSourceResolverTests.swift */, 5A82497C63D507E82FE02693 /* ImageSourceResolverTests.swift */,
8AA914FB8B32B106911FD67B /* LegalDocumentTests.swift */, 8AA914FB8B32B106911FD67B /* LegalDocumentTests.swift */,
4E55893DD92CBFB24F212454 /* PublicStoreDistanceTests.swift */,
747AD255B6D881E011FDF907 /* SessionStateStoreTests.swift */, 747AD255B6D881E011FDF907 /* SessionStateStoreTests.swift */,
160571AB2F8D4DF98C0BE16F /* SmokeTests.swift */, 160571AB2F8D4DF98C0BE16F /* SmokeTests.swift */,
B2CE116C585A7529CF309B3D /* SnackbarCenterTests.swift */, B2CE116C585A7529CF309B3D /* SnackbarCenterTests.swift */,
@@ -621,7 +624,7 @@
); );
name = NotificationServiceExtension; name = NotificationServiceExtension;
productName = NotificationServiceExtension; productName = NotificationServiceExtension;
productReference = C724E02EC34BB1D4352C2FF0 /* NotificationService.appex */; productReference = C724E02EC34BB1D4352C2FF0 /* NotificationServiceExtension.appex */;
productType = "com.apple.product-type.app-extension"; productType = "com.apple.product-type.app-extension";
}; };
66486D25E3A8A75EA8FACE44 /* PediFoodsUITests */ = { 66486D25E3A8A75EA8FACE44 /* PediFoodsUITests */ = {
@@ -671,10 +674,21 @@
BuildIndependentTargetsInParallel = YES; BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1430; LastUpgradeCheck = 1430;
TargetAttributes = { TargetAttributes = {
3027BAE353B414EC70E2A2AC = {
DevelopmentTeam = "";
ProvisioningStyle = Manual;
};
33EB2407F0F758EB98A769D3 = {
DevelopmentTeam = "";
ProvisioningStyle = Manual;
};
66486D25E3A8A75EA8FACE44 = { 66486D25E3A8A75EA8FACE44 = {
DevelopmentTeam = K4E5BZMM4V;
TestTargetID = B3B5CFF77A8ED019D1299ABB; TestTargetID = B3B5CFF77A8ED019D1299ABB;
}; };
B3B5CFF77A8ED019D1299ABB = { B3B5CFF77A8ED019D1299ABB = {
DevelopmentTeam = "";
ProvisioningStyle = Manual;
SystemCapabilities = { SystemCapabilities = {
com.apple.Push = { com.apple.Push = {
enabled = YES; enabled = YES;
@@ -867,6 +881,7 @@
63AB40542AABCC3CB2390B87 /* HomeViewFilteringTests.swift in Sources */, 63AB40542AABCC3CB2390B87 /* HomeViewFilteringTests.swift in Sources */,
22E3BF78B8C2B22D14EABE8B /* ImageSourceResolverTests.swift in Sources */, 22E3BF78B8C2B22D14EABE8B /* ImageSourceResolverTests.swift in Sources */,
2FCB3403A764AD74AC3BD47C /* LegalDocumentTests.swift in Sources */, 2FCB3403A764AD74AC3BD47C /* LegalDocumentTests.swift in Sources */,
901A6A5A00C18FC59F764B85 /* PublicStoreDistanceTests.swift in Sources */,
F760557D906B860D0EA277DE /* SessionStateStoreTests.swift in Sources */, F760557D906B860D0EA277DE /* SessionStateStoreTests.swift in Sources */,
B24B898508C8A8EF4191EC89 /* SmokeTests.swift in Sources */, B24B898508C8A8EF4191EC89 /* SmokeTests.swift in Sources */,
7CC171B83BC4CDEC38B25F8C /* SnackbarCenterTests.swift in Sources */, 7CC171B83BC4CDEC38B25F8C /* SnackbarCenterTests.swift in Sources */,
@@ -986,7 +1001,6 @@
388EFAF707AD295240B9E951 /* Release */ = { 388EFAF707AD295240B9E951 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
APP_ATTEST_ENVIRONMENT = production;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
@@ -994,7 +1008,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2026.0.25.0.0.1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1002,7 +1016,7 @@
INFOPLIST_FILE = PediFoods/Info.plist; INFOPLIST_FILE = PediFoods/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Pedi Foods"; INFOPLIST_KEY_CFBundleDisplayName = "Pedi Foods";
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "PediFoods usa sua localização para mostrar lojas perto de você"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "PediFoods uses your location to show nearby stores and estimate delivery times.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = NO; INFOPLIST_KEY_UILaunchScreen_Generation = NO;
@@ -1031,7 +1045,6 @@
4424276379B7A6A98892CFBC /* Debug */ = { 4424276379B7A6A98892CFBC /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
APP_ATTEST_ENVIRONMENT = development;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
@@ -1039,7 +1052,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2026.0.25.0.0.1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1047,7 +1060,7 @@
INFOPLIST_FILE = PediFoods/Info.plist; INFOPLIST_FILE = PediFoods/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Pedi Foods"; INFOPLIST_KEY_CFBundleDisplayName = "Pedi Foods";
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "PediFoods usa sua localização para mostrar lojas perto de você"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "PediFoods uses your location to show nearby stores and estimate delivery times.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = NO; INFOPLIST_KEY_UILaunchScreen_Generation = NO;

View File

@@ -53,12 +53,17 @@ struct FeaturedStoreCard: View {
Text("(\(store.reviews))") Text("(\(store.reviews))")
.font(.caption) .font(.caption)
.foregroundStyle(AppColors.textMuted) .foregroundStyle(AppColors.textMuted)
Text("·") // Empty distance means "unavailable" - drop the separator
.font(.caption) // with it, otherwise the row ends in a dangling "·".
.foregroundStyle(AppColors.textMuted) // See docs/plans/public-store-distance-consumer.md.
Text(store.distance) if store.distance.isEmpty == false {
.font(.caption) Text("·")
.foregroundStyle(AppColors.textMuted) .font(.caption)
.foregroundStyle(AppColors.textMuted)
Text(store.distance)
.font(.caption)
.foregroundStyle(AppColors.textMuted)
}
} }
} else { } else {
Text(store.statusLabel?.isEmpty == false ? (store.statusLabel ?? "Fechado") : "Fechado") Text(store.statusLabel?.isEmpty == false ? (store.statusLabel ?? "Fechado") : "Fechado")

View File

@@ -28,6 +28,12 @@ struct PublicStoreListItem: Decodable, Sendable, Identifiable {
let deliveryTime: String? let deliveryTime: String?
let deliveryFee: Double? let deliveryFee: Double?
let minOrder: Double? let minOrder: Double?
/// Kilometres from the picked city's centroid to the store - always
/// approximate, since the origin is the city rather than the visitor.
/// The server sends `0` for "unavailable" and never null; optional here
/// only so the app keeps decoding responses from before the field
/// shipped. See docs/plans/public-store-distance-consumer.md.
let distance: Double?
} }
struct PublicStoreDetail: Decodable, Sendable { struct PublicStoreDetail: Decodable, Sendable {
@@ -56,6 +62,9 @@ struct PublicStoreDetail: Decodable, Sendable {
let deliveryPrice: Double? let deliveryPrice: Double?
let minOrder: Double? let minOrder: Double?
let acceptPix: Bool? let acceptPix: Bool?
/// Same contract as `PublicStoreListItem.distance`, but only populated
/// when the detail request carries `state`/`city`.
let distance: Double?
} }
// Maps the public (anonymous) store-detail projection onto the same models // Maps the public (anonymous) store-detail projection onto the same models

View File

@@ -34,13 +34,31 @@ final class PublicLocationService: @unchecked Sendable {
} }
/// No guest token this route is fully public/unauthenticated per the doc. /// No guest token this route is fully public/unauthenticated per the doc.
func fetchStoreDetail(identifier: String) async throws -> PublicStoreDetail { ///
/// `state`/`city` are optional on the server and are what let it resolve
/// the city centroid the store `distance` is measured from; without them
/// the response carries `distance: 0` (see
/// docs/plans/public-store-distance-consumer.md). Defaults to whatever the
/// visitor picked in "ENTREGAR EM:".
func fetchStoreDetail(
identifier: String,
state: String? = GuestLocationStore.shared.selectedState,
city: String? = GuestLocationStore.shared.selectedCity
) async throws -> PublicStoreDetail {
let encodedIdentifier = identifier.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? identifier let encodedIdentifier = identifier.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? identifier
var query: [URLQueryItem] = []
if let state, state.isEmpty == false {
query.append(URLQueryItem(name: "state", value: state))
}
if let city, city.isEmpty == false {
query.append(URLQueryItem(name: "city", value: city))
}
let req = ApiRequest( let req = ApiRequest(
path: "/api/public/store/\(encodedIdentifier)", path: "/api/public/store/\(encodedIdentifier)",
method: "GET", method: "GET",
module: .none, module: .none,
requiresAuth: false, requiresAuth: false,
queryItems: query,
baseURLOverride: ApiConfig.pediFoodsBFFURL baseURLOverride: ApiConfig.pediFoodsBFFURL
) )
let envelope: ApiEnvelope<PublicStoreDetail> = try await client.send(req) let envelope: ApiEnvelope<PublicStoreDetail> = try await client.send(req)

View File

@@ -177,7 +177,7 @@ extension HomeView {
extension StoreSummary { extension StoreSummary {
/// Maps the public-locator DTO onto the same model HomeView already /// Maps the public-locator DTO onto the same model HomeView already
/// renders distance/positiveReviews don't exist in that response. /// renders positiveReviews doesn't exist in that response.
init(publicItem: PublicStoreListItem) { init(publicItem: PublicStoreListItem) {
self.id = publicItem.id self.id = publicItem.id
self.name = publicItem.name ?? "Loja" self.name = publicItem.name ?? "Loja"
@@ -189,7 +189,10 @@ extension StoreSummary {
self.positiveReviews = nil self.positiveReviews = nil
self.deliveryTime = publicItem.deliveryTime self.deliveryTime = publicItem.deliveryTime
self.deliveryFee = publicItem.deliveryFee self.deliveryFee = publicItem.deliveryFee
self.distance = nil // The contract uses `0` for "no distance available"; normalize it to
// nil here so every downstream consumer (card label, max-distance
// filter) sees one representation of "unknown".
self.distance = (publicItem.distance ?? 0) > 0 ? publicItem.distance : nil
self.isOpen = publicItem.isOpen self.isOpen = publicItem.isOpen
self.statusLabel = publicItem.statusLabel self.statusLabel = publicItem.statusLabel
} }

View File

@@ -82,8 +82,13 @@ extension HomeView {
resolveStoreMediaURL(appState.profile.profilePicture) resolveStoreMediaURL(appState.profile.profilePicture)
} }
/// Empty string means "no distance to show" - the caller omits the whole
/// segment rather than labelling it. Per the API contract
/// (docs/plans/public-store-distance-consumer.md) the server never sends
/// null and uses `0` for "unavailable", so both that and a missing value
/// collapse to the same empty result.
func formatDistance(_ distance: Double?) -> String { func formatDistance(_ distance: Double?) -> String {
guard let distance else { return "Distância indisponível" } guard let distance, distance > 0 else { return "" }
if distance >= 1 { if distance >= 1 {
return String(format: "%.1f km", distance) return String(format: "%.1f km", distance)
} }

View File

@@ -109,8 +109,13 @@ extension StoreDetailView {
} }
HStack(spacing: 0) { HStack(spacing: 0) {
statItem(title: "DISTÂNCIA", value: distanceValueLabel) // No distance available: drop the tile and its divider
Divider().frame(height: 34) // instead of showing a placeholder - see
// docs/plans/public-store-distance-consumer.md.
if hasStoreDistance {
statItem(title: "DISTÂNCIA", value: distanceValueLabel)
Divider().frame(height: 34)
}
if let deliveryTime = info?.deliveryTime { if let deliveryTime = info?.deliveryTime {
statItem(title: "TEMPO MIN.", value: deliveryTime+" min.") statItem(title: "TEMPO MIN.", value: deliveryTime+" min.")
} else { } else {

View File

@@ -57,12 +57,14 @@ extension StoreDetailView {
return "R$ --" return "R$ --"
} }
/// Already formatted upstream by `HomeView.formatDistance`, which returns
/// an empty string when there's no distance to show.
var distanceValueLabel: String { var distanceValueLabel: String {
let raw = (storeDistance ?? "").trimmingCharacters(in: .whitespacesAndNewlines) (storeDistance ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
if raw.isEmpty { }
return "--"
} var hasStoreDistance: Bool {
return raw distanceValueLabel.isEmpty == false
} }
var isStoreOpen: Bool { var isStoreOpen: Bool {

View File

@@ -63,5 +63,17 @@ func formatDistanceSwitchesUnitsAtOneKm() {
#expect(view.formatDistance(0.5) == "500 m") #expect(view.formatDistance(0.5) == "500 m")
#expect(view.formatDistance(1.0) == "1.0 km") #expect(view.formatDistance(1.0) == "1.0 km")
#expect(view.formatDistance(2.3) == "2.3 km") #expect(view.formatDistance(2.3) == "2.3 km")
#expect(view.formatDistance(nil) == "Distância indisponível") }
/// Contract (docs/plans/public-store-distance-consumer.md): the API never
/// sends null - `0` means "no distance available". Both that and a missing
/// value render as an empty string so the caller omits the segment
/// entirely, instead of showing "Distância indisponível" or "0 m".
@Test("formatDistance renders nothing when the distance is missing or zero")
@MainActor
func formatDistanceIsEmptyWhenUnavailable() {
let view = makeHomeView()
#expect(view.formatDistance(nil) == "")
#expect(view.formatDistance(0) == "")
#expect(view.formatDistance(-1) == "")
} }

View File

@@ -0,0 +1,49 @@
import Foundation
import Testing
@testable import PediFoods
/// Covers the public-locator distance contract documented in
/// docs/plans/public-store-distance-consumer.md: the server sends
/// `distance` as a number (never null) where `0` means "unavailable", and
/// the app collapses that to `nil` so a single representation of "unknown"
/// reaches the card label and the max-distance filter.
private func decode<T: Decodable>(_ type: T.Type, _ json: String) throws -> T {
try JSONDecoder().decode(T.self, from: Data(json.utf8))
}
@Test("PublicStoreListItem decodes the distance field")
func publicStoreListItemDecodesDistance() throws {
let item = try decode(PublicStoreListItem.self, #"{"id":"1","distance":3.4}"#)
#expect(item.distance == 3.4)
}
/// The field ships on the backend after the app - a response without it
/// must still decode rather than throwing.
@Test("PublicStoreListItem still decodes a response with no distance field")
func publicStoreListItemDecodesWithoutDistance() throws {
let item = try decode(PublicStoreListItem.self, #"{"id":"1"}"#)
#expect(item.distance == nil)
}
@Test("PublicStoreDetail decodes the distance field")
func publicStoreDetailDecodesDistance() throws {
let detail = try decode(PublicStoreDetail.self, #"{"id":"1","distance":1.2}"#)
#expect(detail.distance == 1.2)
let without = try decode(PublicStoreDetail.self, #"{"id":"1"}"#)
#expect(without.distance == nil)
}
@Test("StoreSummary carries a real public distance through")
func storeSummaryKeepsPublicDistance() throws {
let item = try decode(PublicStoreListItem.self, #"{"id":"1","name":"A","distance":2.5}"#)
#expect(StoreSummary(publicItem: item).distance == 2.5)
}
@Test("StoreSummary maps the unavailable sentinel and a missing distance to nil")
func storeSummaryNormalizesUnavailableDistance() throws {
let zero = try decode(PublicStoreListItem.self, #"{"id":"1","name":"A","distance":0}"#)
#expect(StoreSummary(publicItem: zero).distance == nil)
let absent = try decode(PublicStoreListItem.self, #"{"id":"1","name":"A"}"#)
#expect(StoreSummary(publicItem: absent).distance == nil)
}