[2026-07-resubmission] Add guest browsing flow with App Attest session for App Review resubmission

Adds a pre-login public store locator (guest session via DeviceCheck/App
Attest, keychain-backed token storage) so the app no longer forces sign-in
before showing any content, plus updated support URL metadata.
This commit is contained in:
Daniel Arantes Loverde
2026-07-30 11:35:25 -03:00
parent 3e93196b92
commit 017bd7168f
21 changed files with 1140 additions and 136 deletions

View File

@@ -0,0 +1,174 @@
import SwiftUI
/// State -> city picker for anonymous browsing (public store locator).
/// Replaces AddressesView in the address-picker modal when the user is
/// not authenticated see docs/plans/public-store-locator-sdd.md.
struct PublicLocationPickerView: View {
@Binding var appState: AppState
@Environment(\.dismiss) var dismiss
private enum Step {
case state
case city
}
@State private var step: Step = .state
@State private var locations: PublicLocationsResult = [:]
@State private var selectedState: String? = nil
@State private var isLoading = false
@State private var errorMessage: String? = nil
private var states: [String] {
locations.keys.sorted()
}
private var cities: [String] {
guard let selectedState else { return [] }
return (locations[selectedState] ?? []).sorted()
}
var body: some View {
ZStack {
AppColors.backgroundLight.ignoresSafeArea()
VStack(spacing: 20) {
header
if isLoading {
ProgressView()
.padding(.top, 40)
} else if let errorMessage {
VStack(spacing: 12) {
Text(errorMessage)
.font(AppTypography.body)
.foregroundStyle(AppColors.textMuted)
.multilineTextAlignment(.center)
Button("Tentar novamente") {
Task { await loadLocations() }
}
.font(AppTypography.heading3)
.foregroundStyle(AppColors.primary)
}
.padding(.horizontal, 20)
.padding(.top, 40)
} else {
list
}
Spacer(minLength: 0)
}
.padding(.top, 18)
}
.navigationBarBackButtonHidden(true)
.appHiddenNavigationBar()
.task {
await loadLocations()
}
}
private var header: some View {
ZStack {
Text(step == .state ? "Escolha seu estado" : "Escolha sua cidade")
.font(AppTypography.heading2)
.foregroundStyle(AppColors.textPrimary)
HStack {
Button(action: back) {
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()
}
}
.padding(.horizontal, 20)
}
private var list: some View {
ScrollView(showsIndicators: false) {
LazyVStack(spacing: 12) {
switch step {
case .state:
ForEach(states, id: \.self) { state in
rowButton(title: state) {
selectedState = state
step = .city
}
}
case .city:
ForEach(cities, id: \.self) { city in
rowButton(title: city) {
confirmSelection(city: city)
}
}
}
}
.padding(.horizontal, 20)
.padding(.top, 8)
}
}
private func rowButton(title: String, action: @escaping () -> Void) -> some View {
Button(action: action) {
HStack {
Text(title)
.font(AppTypography.body)
.foregroundStyle(AppColors.textPrimary)
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(AppColors.textMuted)
}
.padding(.horizontal, 18)
.padding(.vertical, 16)
.background(AppColors.surface)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
}
.buttonStyle(.plain)
}
private func back() {
switch step {
case .city:
step = .state
errorMessage = nil
case .state:
dismiss()
}
}
private func confirmSelection(city: String) {
guard let selectedState else { return }
GuestLocationStore.shared.selectedState = selectedState
GuestLocationStore.shared.selectedCity = city
appState.address.display = "\(city), \(selectedState)"
appState.address.onboardingMessage = nil
dismiss()
}
@MainActor
private func loadLocations() async {
isLoading = true
errorMessage = nil
do {
locations = try await PublicLocationService.shared.fetchLocations()
if locations.isEmpty {
errorMessage = "Nenhum estado disponível no momento."
}
} catch {
errorMessage = "Não foi possível carregar. Tente novamente."
}
isLoading = false
}
}
#Preview {
NavigationStack {
PublicLocationPickerView(appState: .constant(AppState()))
}
}