[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:
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 56;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -79,9 +79,10 @@
|
||||
7CC171B83BC4CDEC38B25F8C /* SnackbarCenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2CE116C585A7529CF309B3D /* SnackbarCenterTests.swift */; };
|
||||
7D7BF04765D6CA6AC4F11DDD /* StoreCatalogNormalizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18EC38EEE1E7105BC21E354C /* StoreCatalogNormalizerTests.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 */; };
|
||||
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 */; };
|
||||
918BCBC1EAD7EE134DB9742D /* CheckoutTypesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FF15CEE5B137074E6B2489 /* CheckoutTypesTests.swift */; };
|
||||
926E0C4DD3C82485D4262A62 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B2F3558C58CD1E71F0F3A0 /* ContentView.swift */; };
|
||||
@@ -168,7 +169,7 @@
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
811217F88AA350A70CC22479 /* NotificationService.appex in Embed Foundation Extensions */,
|
||||
811217F88AA350A70CC22479 /* NotificationServiceExtension.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -210,6 +211,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -228,7 +230,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -241,7 +243,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -250,7 +252,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -285,7 +287,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -344,7 +346,7 @@
|
||||
126A579C448C784867DC3AB1 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C724E02EC34BB1D4352C2FF0 /* NotificationService.appex */,
|
||||
C724E02EC34BB1D4352C2FF0 /* NotificationServiceExtension.appex */,
|
||||
7A6D74FD2D2F2F79349EA04E /* PediFoods.app */,
|
||||
6DD1D8390E57439A347F9E07 /* PediFoodsTests.xctest */,
|
||||
8AF987AB319512C123DFD558 /* PediFoodsUITests.xctest */,
|
||||
@@ -446,6 +448,7 @@
|
||||
23B6B814027518969EB04341 /* HomeViewFilteringTests.swift */,
|
||||
5A82497C63D507E82FE02693 /* ImageSourceResolverTests.swift */,
|
||||
8AA914FB8B32B106911FD67B /* LegalDocumentTests.swift */,
|
||||
4E55893DD92CBFB24F212454 /* PublicStoreDistanceTests.swift */,
|
||||
747AD255B6D881E011FDF907 /* SessionStateStoreTests.swift */,
|
||||
160571AB2F8D4DF98C0BE16F /* SmokeTests.swift */,
|
||||
B2CE116C585A7529CF309B3D /* SnackbarCenterTests.swift */,
|
||||
@@ -621,7 +624,7 @@
|
||||
);
|
||||
name = NotificationServiceExtension;
|
||||
productName = NotificationServiceExtension;
|
||||
productReference = C724E02EC34BB1D4352C2FF0 /* NotificationService.appex */;
|
||||
productReference = C724E02EC34BB1D4352C2FF0 /* NotificationServiceExtension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
66486D25E3A8A75EA8FACE44 /* PediFoodsUITests */ = {
|
||||
@@ -671,10 +674,21 @@
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1430;
|
||||
TargetAttributes = {
|
||||
3027BAE353B414EC70E2A2AC = {
|
||||
DevelopmentTeam = "";
|
||||
ProvisioningStyle = Manual;
|
||||
};
|
||||
33EB2407F0F758EB98A769D3 = {
|
||||
DevelopmentTeam = "";
|
||||
ProvisioningStyle = Manual;
|
||||
};
|
||||
66486D25E3A8A75EA8FACE44 = {
|
||||
DevelopmentTeam = K4E5BZMM4V;
|
||||
TestTargetID = B3B5CFF77A8ED019D1299ABB;
|
||||
};
|
||||
B3B5CFF77A8ED019D1299ABB = {
|
||||
DevelopmentTeam = "";
|
||||
ProvisioningStyle = Manual;
|
||||
SystemCapabilities = {
|
||||
com.apple.Push = {
|
||||
enabled = YES;
|
||||
@@ -867,6 +881,7 @@
|
||||
63AB40542AABCC3CB2390B87 /* HomeViewFilteringTests.swift in Sources */,
|
||||
22E3BF78B8C2B22D14EABE8B /* ImageSourceResolverTests.swift in Sources */,
|
||||
2FCB3403A764AD74AC3BD47C /* LegalDocumentTests.swift in Sources */,
|
||||
901A6A5A00C18FC59F764B85 /* PublicStoreDistanceTests.swift in Sources */,
|
||||
F760557D906B860D0EA277DE /* SessionStateStoreTests.swift in Sources */,
|
||||
B24B898508C8A8EF4191EC89 /* SmokeTests.swift in Sources */,
|
||||
7CC171B83BC4CDEC38B25F8C /* SnackbarCenterTests.swift in Sources */,
|
||||
@@ -986,7 +1001,6 @@
|
||||
388EFAF707AD295240B9E951 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APP_ATTEST_ENVIRONMENT = production;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
@@ -994,7 +1008,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2026.0.25.0.0.1;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1002,7 +1016,7 @@
|
||||
INFOPLIST_FILE = PediFoods/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Pedi Foods";
|
||||
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_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = NO;
|
||||
@@ -1031,7 +1045,6 @@
|
||||
4424276379B7A6A98892CFBC /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APP_ATTEST_ENVIRONMENT = development;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
@@ -1039,7 +1052,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2026.0.25.0.0.1;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1047,7 +1060,7 @@
|
||||
INFOPLIST_FILE = PediFoods/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Pedi Foods";
|
||||
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_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = NO;
|
||||
|
||||
@@ -53,6 +53,10 @@ struct FeaturedStoreCard: View {
|
||||
Text("(\(store.reviews))")
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
// Empty distance means "unavailable" - drop the separator
|
||||
// with it, otherwise the row ends in a dangling "·".
|
||||
// See docs/plans/public-store-distance-consumer.md.
|
||||
if store.distance.isEmpty == false {
|
||||
Text("·")
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
@@ -60,6 +64,7 @@ struct FeaturedStoreCard: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(store.statusLabel?.isEmpty == false ? (store.statusLabel ?? "Fechado") : "Fechado")
|
||||
.font(AppTypography.heading3)
|
||||
|
||||
@@ -28,6 +28,12 @@ struct PublicStoreListItem: Decodable, Sendable, Identifiable {
|
||||
let deliveryTime: String?
|
||||
let deliveryFee: 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 {
|
||||
@@ -56,6 +62,9 @@ struct PublicStoreDetail: Decodable, Sendable {
|
||||
let deliveryPrice: Double?
|
||||
let minOrder: Double?
|
||||
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
|
||||
|
||||
@@ -34,13 +34,31 @@ final class PublicLocationService: @unchecked Sendable {
|
||||
}
|
||||
|
||||
/// 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
|
||||
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(
|
||||
path: "/api/public/store/\(encodedIdentifier)",
|
||||
method: "GET",
|
||||
module: .none,
|
||||
requiresAuth: false,
|
||||
queryItems: query,
|
||||
baseURLOverride: ApiConfig.pediFoodsBFFURL
|
||||
)
|
||||
let envelope: ApiEnvelope<PublicStoreDetail> = try await client.send(req)
|
||||
|
||||
@@ -177,7 +177,7 @@ extension HomeView {
|
||||
|
||||
extension StoreSummary {
|
||||
/// 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) {
|
||||
self.id = publicItem.id
|
||||
self.name = publicItem.name ?? "Loja"
|
||||
@@ -189,7 +189,10 @@ extension StoreSummary {
|
||||
self.positiveReviews = nil
|
||||
self.deliveryTime = publicItem.deliveryTime
|
||||
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.statusLabel = publicItem.statusLabel
|
||||
}
|
||||
|
||||
@@ -82,8 +82,13 @@ extension HomeView {
|
||||
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 {
|
||||
guard let distance else { return "Distância indisponível" }
|
||||
guard let distance, distance > 0 else { return "" }
|
||||
if distance >= 1 {
|
||||
return String(format: "%.1f km", distance)
|
||||
}
|
||||
|
||||
@@ -109,8 +109,13 @@ extension StoreDetailView {
|
||||
}
|
||||
|
||||
HStack(spacing: 0) {
|
||||
// No distance available: drop the tile and its divider
|
||||
// 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 {
|
||||
statItem(title: "TEMPO MIN.", value: deliveryTime+" min.")
|
||||
} else {
|
||||
|
||||
@@ -57,12 +57,14 @@ extension StoreDetailView {
|
||||
return "R$ --"
|
||||
}
|
||||
|
||||
/// Already formatted upstream by `HomeView.formatDistance`, which returns
|
||||
/// an empty string when there's no distance to show.
|
||||
var distanceValueLabel: String {
|
||||
let raw = (storeDistance ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if raw.isEmpty {
|
||||
return "--"
|
||||
(storeDistance ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
return raw
|
||||
|
||||
var hasStoreDistance: Bool {
|
||||
distanceValueLabel.isEmpty == false
|
||||
}
|
||||
|
||||
var isStoreOpen: Bool {
|
||||
|
||||
@@ -63,5 +63,17 @@ func formatDistanceSwitchesUnitsAtOneKm() {
|
||||
#expect(view.formatDistance(0.5) == "500 m")
|
||||
#expect(view.formatDistance(1.0) == "1.0 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) == "")
|
||||
}
|
||||
|
||||
49
PediFoodsTests/PublicStoreDistanceTests.swift
Normal file
49
PediFoodsTests/PublicStoreDistanceTests.swift
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user