feat(app): integrate Atomenta Feature Control bootstrap, cache and resume refresh

This commit is contained in:
Daniel Arantes Loverde
2026-04-16 14:48:39 -03:00
parent f8d2fb8eb7
commit 7f7d414e6c
6 changed files with 433 additions and 1 deletions

View File

@@ -1,3 +1,4 @@
import Foundation
import SwiftUI
struct ContentView: View {
@@ -15,6 +16,7 @@ struct ContentView: View {
@State private var sessionExpiredObserver: NSObjectProtocol?
#endif
@State var cartResetObserver: Any?
@State var appResumeObserver: Any?
#if os(Android)
@State var snackbarCenter = SnackbarCenter.shared
#else
@@ -80,14 +82,26 @@ struct ContentView: View {
}
}
}
.onChange(of: appState.profile.id) { _, _ in
Task { @MainActor in
await refreshFeatureFlags(forceRefresh: true)
}
}
.onChange(of: appState.cart.storeId) { _, _ in
Task { @MainActor in
await refreshFeatureFlags(forceRefresh: true)
}
}
.onAppear {
attachCartResetObserverIfNeeded()
attachAppResumeObserverIfNeeded()
#if os(iOS)
attachSessionExpiredObserverIfNeeded()
#endif
}
.onDisappear {
detachCartResetObserver()
detachAppResumeObserver()
#if os(iOS)
detachSessionExpiredObserver()
#endif
@@ -179,6 +193,7 @@ struct ContentView: View {
// Keep local state when backend refresh fails transiently.
}
await refreshFeatureFlags(forceRefresh: false)
isBootstrappingSession = false
}
@@ -189,7 +204,9 @@ struct ContentView: View {
object: nil,
queue: nil
) { _ in
appState.cart = CartState()
Task { @MainActor in
appState.cart = CartState()
}
}
}
@@ -199,6 +216,26 @@ struct ContentView: View {
self.cartResetObserver = nil
}
private func attachAppResumeObserverIfNeeded() {
guard appResumeObserver == nil else { return }
appResumeObserver = NotificationCenter.default.addObserver(
forName: .appDidResume,
object: nil,
queue: nil
) { _ in
Task { @MainActor in
guard root == .main else { return }
await refreshFeatureFlags(forceRefresh: true)
}
}
}
private func detachAppResumeObserver() {
guard let appResumeObserver else { return }
NotificationCenter.default.removeObserver(appResumeObserver)
self.appResumeObserver = nil
}
@MainActor
private func hydrateAppState(with customer: CustomerProfile) {
appState.profile.id = customer.id
@@ -206,6 +243,7 @@ struct ContentView: View {
appState.profile.email = customer.email
appState.profile.phone = customer.phoneNumber ?? ""
appState.profile.profilePicture = customer.profilePicture ?? ""
appState.favorites.storeIds = Set(customer.favorites ?? [])
SessionStateStore.setActiveUserKey(
SessionStateStore.makeUserKey(profileId: customer.id, email: customer.email)
)
@@ -299,6 +337,46 @@ struct ContentView: View {
scheduleDisableAuthEntryPreparation()
}
@MainActor
private func refreshFeatureFlags(forceRefresh: Bool) async {
guard root == .main else { return }
let subjectType: String
let subjectId: String
if let profileId = appState.profile.id?.trimmingCharacters(in: .whitespacesAndNewlines), profileId.isEmpty == false {
subjectType = "customer"
subjectId = profileId
} else {
subjectType = "anonymous"
subjectId = "anonymous-device"
}
var attrs: [String: String] = [:]
let addressLabel = appState.address.display.trimmingCharacters(in: .whitespacesAndNewlines)
if addressLabel.isEmpty == false, addressLabel.lowercased() != "defina seu endereco" {
attrs["addressLabel"] = addressLabel
}
let context = FeatureControlEvaluationContext(
subjectType: subjectType,
subjectId: subjectId,
storeId: appState.cart.storeId,
attributes: attrs
)
let snapshot = await FeatureControlService.shared.evaluate(
context: context,
jwt: appState.session.jwt,
forceRefresh: forceRefresh
)
appState.featureFlags = snapshot
await FeatureControlService.shared.sendExposureEvents(
snapshot: snapshot,
context: context,
jwt: appState.session.jwt
)
}
private func scheduleDisableAuthEntryPreparation() {
Task { @MainActor in
try? await Task.sleep(nanoseconds: 900_000_000)