diff --git a/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift b/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift index b5426628..577cf42e 100644 --- a/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift +++ b/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift @@ -1,31 +1,45 @@ import Confidence import SwiftUI + @main struct ConfidenceDemoApp: App { @AppStorage("loggedUser") private var loggedUser: String? + @AppStorage("appVersion") private var appVersion = 0 private let confidence: Confidence private let flaggingState = ExperimentationFlags() + private let secret = ProcessInfo.processInfo.environment["CLIENT_SECRET"] ?? "" init() { + @AppStorage("appVersion") var appVersion = 0 + @AppStorage("loggedUser") var loggedUser: String? + appVersion = appVersion + 1 // Simulate update of the app on every new run + var context = ["app_version": ConfidenceValue.init(integer: appVersion)] + if let user = loggedUser { + context["user_id"] = ConfidenceValue.init(string: user) + } + confidence = Confidence - .Builder(clientSecret: ProcessInfo.processInfo.environment["CLIENT_SECRET"] ?? "", loggerLevel: .TRACE) + .Builder(clientSecret: secret, loggerLevel: .TRACE) + .withContext(initialContext: context) .build() - - if let loggedUser = loggedUser { - confidence.putContextLocal(context: ["user_id": ConfidenceValue.init(string: loggedUser)], removeKeys: []) - do { - try confidence.activate() - flaggingState.state = .ready - } catch { - print("Error during activation of Confidence") - flaggingState.state = .error(ExperimentationFlags.CustomError(message: error.localizedDescription)) - } + do { + // DANGER: here we are activating anything in storage, could be data of a different user + try confidence.activate() // Activating the existing cache, in case the new fetch fails i.e. offline mode + } catch { + flaggingState.state = .error(ExperimentationFlags.CustomError(message: error.localizedDescription)) } + flaggingState.color = ContentView.getColor(color: confidence.getValue(key: "swift-demoapp.color", defaultValue: "Gray")) + + self.appVersion = appVersion + self.loggedUser = loggedUser + updateConfidence() } var body: some Scene { WindowGroup { + Text("Client secret: \(secret)") + .font(.caption) if loggedUser == nil { LoginView(confidence: confidence) .environmentObject(flaggingState) @@ -35,9 +49,23 @@ struct ConfidenceDemoApp: App { } } } + + private func updateConfidence() { + Task { + do { + flaggingState.state = .loading + try await Task.sleep(nanoseconds: 2 * 1_000_000_000) + try await confidence.fetchAndActivate() + flaggingState.state = .ready + } catch { + flaggingState.state = .error(ExperimentationFlags.CustomError(message: error.localizedDescription)) + } + } + } } class ExperimentationFlags: ObservableObject { + var color: Color = .red // This is set on applicaaton start, and reset on user logout @Published var state: State = .notReady enum State: Equatable { diff --git a/ConfidenceDemoApp/ConfidenceDemoApp/ContentView.swift b/ConfidenceDemoApp/ConfidenceDemoApp/ContentView.swift index 6aec74cd..746ce112 100644 --- a/ConfidenceDemoApp/ConfidenceDemoApp/ContentView.swift +++ b/ConfidenceDemoApp/ConfidenceDemoApp/ContentView.swift @@ -3,42 +3,53 @@ import Confidence import Combine struct ContentView: View { - @EnvironmentObject var status: ExperimentationFlags + @EnvironmentObject var flaggingState: ExperimentationFlags @AppStorage("loggedUser") private var loggedUser: String? @State var isLoggingOut: Bool = false @State var loggedOut: Bool = false + @State var textColor: Color = .red private let confidence: Confidence - init(confidence: Confidence) { + init(confidence: Confidence, color: Color? = nil) { self.confidence = confidence } var body: some View { NavigationStack { - if (status.state == .loading && !isLoggingOut) { + if (flaggingState.state == .loading && !isLoggingOut) { VStack { Spacer() - Text("Welcome \(loggedUser ?? "?")") + Text("Login successful: \(loggedUser ?? "?")") Text("We are preparing your experience...") ProgressView() - Spacer() } } else { - VStack { - Text("Hello World!") - .font(.largeTitle) - .foregroundStyle(ContentView.getColor(color: confidence.getValue(key: "swift-demoapp.color", defaultValue: "Gray"))) + if (flaggingState.state == .ready) { + VStack { + Spacer() + if let user = loggedUser { + let eval = confidence.getEvaluation(key: "swift-demoapp.color", defaultValue: "Gray") + Text("Hello \(user)") + .font(.largeTitle) + // Any change of flagState triggers a re-read of the value + // The "state = ready" wrapper makes sure only the reconciled value is shown at any time + .foregroundStyle(ContentView.getColor(color: eval.value)) + .padding() + } + } + } + NavigationLink(destination: AboutPage()) { + Text("Navigate") } - .padding() Button("Logout") { isLoggingOut = true - loggedUser = nil - status.state = .loading + flaggingState.state = .loading + flaggingState.color = .gray Task { await confidence.removeContext(key: "user_id") - status.state = .ready + flaggingState.state = .ready } loggedUser = nil loggedOut = true @@ -46,11 +57,36 @@ struct ContentView: View { .navigationDestination(isPresented: $loggedOut) { LoginView(confidence: confidence) } + Spacer() + } + ZStack { + VStack { + Spacer() + Text("This text is set on onAppear, doesn't wait for reconciliation") + .font(.caption) + .foregroundStyle(textColor) // This is set on onAppear and not changing, although it might show different values within the session if the user exit and re-enter ContentView (try tapping on Navigate and go Back) + Text("This text dynamically changes on flags fetch") + .font(.caption) + // Any change of flagState triggers a re-read of the value + .foregroundStyle(ContentView.getColor(color: confidence.getValue(key: "swift-demoapp.color", defaultValue: "Gray"))) + Text("This text is fixed from user session start, and before any flags fetch") + .font(.caption) + // Any change of flagState triggers a re-read of the value + .foregroundStyle(flaggingState.color) + } + }.onAppear { + let eval = confidence.getEvaluation(key: "swift-demoapp.color", defaultValue: "Gray") + /* + Due to syntetic delay, the new context hasn't entered the SDK yet se we are operating with + the last seen context before the view loads + */ + print(">> Evaluation reason: \(eval)") + textColor = ContentView.getColor(color: eval.value) } } } - private static func getColor(color: String) -> Color { + public static func getColor(color: String) -> Color { switch color { case "Green": .green @@ -63,3 +99,9 @@ struct ContentView: View { } } } + +struct AboutPage: View { + var body: some View { + Text("About Page") + } +} diff --git a/ConfidenceDemoApp/ConfidenceDemoApp/LoginView.swift b/ConfidenceDemoApp/ConfidenceDemoApp/LoginView.swift index f0469dd5..0abd50ee 100644 --- a/ConfidenceDemoApp/ConfidenceDemoApp/LoginView.swift +++ b/ConfidenceDemoApp/ConfidenceDemoApp/LoginView.swift @@ -2,7 +2,7 @@ import SwiftUI import Confidence struct LoginView: View { - @EnvironmentObject var status: ExperimentationFlags + @EnvironmentObject var flaggingState: ExperimentationFlags @AppStorage("loggedUser") private var loggedUser: String? @State private var loginCompleted = false @State private var flagsLoaded = false @@ -19,17 +19,23 @@ struct LoginView: View { VStack { Spacer() Button("Login as user1") { - status.state = .loading + do { + try confidence.activate() // Activating the existing cache, in case the new fetch fails i.e. offline mode + } catch { + flaggingState.state = .error(ExperimentationFlags.CustomError(message: error.localizedDescription)) + } + flaggingState.color = ContentView.getColor(color: confidence.getValue(key: "swift-demoapp.color", defaultValue: "Gray")) // Load flags Task { - try? await Task.sleep(nanoseconds: 5 * 1_000_000_000) // Sleep for 3 seconds + flaggingState.state = .loading + try? await Task.sleep(nanoseconds: 5 * 1_000_000_000) await confidence.putContext(context: ["user_id": .init(string: "user1")]) - status.state = .ready + flaggingState.state = .ready } // Load everything else for this user Task { loggingIn = true - try? await Task.sleep(nanoseconds: 1 * 1_000_000_000) // Sleep for 3 seconds + try? await Task.sleep(nanoseconds: 1 * 1_000_000_000) loggedUser = "user1" loggingIn = false loginCompleted = true @@ -45,10 +51,4 @@ struct LoginView: View { } } } - private var combinedBinding: Binding { - Binding( - get: { loginCompleted && flagsLoaded }, - set: { _ in } // No-op; navigation state is derived from other properties - ) - } } diff --git a/Sources/Confidence/Confidence.swift b/Sources/Confidence/Confidence.swift index 4b4f0127..d60b762c 100644 --- a/Sources/Confidence/Confidence.swift +++ b/Sources/Confidence/Confidence.swift @@ -64,10 +64,6 @@ public class Confidence: ConfidenceEventSender { } let savedFlags = try storage.load(defaultValue: FlagResolution.EMPTY) cache = savedFlags - debugLogger?.logMessage( - message: "[Activating stored cache]", - isWarning: false - ) } }