[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

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

View File

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

View File

@@ -109,8 +109,13 @@ extension StoreDetailView {
}
HStack(spacing: 0) {
statItem(title: "DISTÂNCIA", value: distanceValueLabel)
Divider().frame(height: 34)
// 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 {

View File

@@ -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 "--"
}
return raw
(storeDistance ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
}
var hasStoreDistance: Bool {
distanceValueLabel.isEmpty == false
}
var isStoreOpen: Bool {