[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

@@ -35,7 +35,7 @@ final class ApiService {
do {
return try await client.send(req)
} catch let error as NetworkError {
if case .unauthorized(let message) = error {
if canTriggerSessionExpiry(for: req), case .unauthorized(let message) = error {
expireSession(message)
throw ApiServiceError.sessionExpired(message)
}
@@ -45,13 +45,22 @@ final class ApiService {
private func sendEnvelope<T: Decodable>(_ req: ApiRequest) async throws -> ApiEnvelope<T> {
let envelope: ApiEnvelope<T> = try await send(req)
if isSessionExpiredEnvelope(envelope) {
if canTriggerSessionExpiry(for: req), isSessionExpiredEnvelope(envelope) {
expireSession(envelope.message)
throw ApiServiceError.sessionExpired(envelope.message)
}
return envelope
}
/// A request that never carried the customer JWT (public/unauthenticated
/// calls) can never mean *the customer's* session expired an unrelated
/// error (e.g. Atomenta's module-token check) must not force-logout a
/// user, anonymous or not, just because its error code happens to
/// contain the substring "token". See app-migrate-atomenta-calls-to-pedifoods-bff.md.
private func canTriggerSessionExpiry(for req: ApiRequest) -> Bool {
req.requiresAuth && tokenStore.jwt != nil
}
private func isSessionExpiredEnvelope<T>(_ envelope: ApiEnvelope<T>) -> Bool {
guard envelope.error else { return false }
let code = (envelope.code ?? "").lowercased()
@@ -96,6 +105,16 @@ final class ApiService {
if let birthDate, birthDate.isEmpty == false {
payload["birthDate"] = birthDate
}
// If the visitor picked a state/city via the public locator before
// signing up, forward it so the backend can set it as the account's
// default city. NOTE: as of this writing Atomenta's customer create
// controller only reads name/email/phoneNumber these two fields
// are a no-op server-side until that controller is updated to
// persist them (see docs/plans/app-migrate-atomenta-calls-to-pedifoods-bff.md).
if let state = GuestLocationStore.shared.selectedState, let city = GuestLocationStore.shared.selectedCity {
payload["defaultState"] = state
payload["defaultCity"] = city
}
let body = try JSONEncoder().encode(payload)
let req = ApiRequest(path: "/api/customer", method: "POST", module: .customer, requiresAuth: false, body: body)
return try await sendEnvelope(req)
@@ -308,7 +327,10 @@ final class ApiService {
return cached
}
let req = ApiRequest(path: "/api/public/categories", method: "GET", module: .none, requiresAuth: false)
// Open endpoint, no guest session needed, but it lives on the BFF
// domain (pedifoods.com.br), not Atomenta see
// docs/plans/app-migrate-atomenta-calls-to-pedifoods-bff.md.
let req = ApiRequest(path: "/api/public/categories", method: "GET", module: .none, requiresAuth: false, baseURLOverride: ApiConfig.pediFoodsBFFURL)
let envelope: ApiEnvelope<[PublicCategory]> = try await sendEnvelope(req)
if envelope.error == false {
AppContentCache.shared.set(envelope, for: publicCategoriesCacheKey, ttl: AppCacheTTL.twoHours)
@@ -447,6 +469,11 @@ final class ApiService {
return try await sendEnvelope(req)
}
func deleteAccount() async throws -> ApiEnvelope<EmptyResult> {
let req = ApiRequest(path: "/api/customer/account", method: "DELETE", module: .customer, requiresAuth: true)
return try await sendEnvelope(req)
}
func changePaymentMethod(storeId: String, orderId: String, payload: ChangePaymentMethodPayload) async throws -> ApiEnvelope<ChangePaymentMethodResult> {
let body = try JSONEncoder().encode(payload)
let req = ApiRequest(path: "/api/store/\(storeId)/orders/\(orderId)/payment-method", method: "PATCH", module: .store, requiresAuth: true, body: body)