[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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user