From 37c25ded61f7923672d8ea832d4fca0937842051 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Fri, 29 Nov 2024 15:00:47 +0100 Subject: [PATCH] feat: Demo App emulates login scenarios --- .../project.pbxproj | 4 + .../ConfidenceDemoApp/ConfidenceDemoApp.swift | 75 ++++++++++--------- .../ConfidenceDemoApp/ContentView.swift | 72 ++++++++---------- .../ConfidenceDemoApp/LoginView.swift | 54 +++++++++++++ Sources/Confidence/Confidence.swift | 14 ++-- Sources/Confidence/DebugLogger.swift | 5 ++ 6 files changed, 142 insertions(+), 82 deletions(-) create mode 100644 ConfidenceDemoApp/ConfidenceDemoApp/LoginView.swift diff --git a/ConfidenceDemoApp/ConfidenceDemoApp.xcodeproj/project.pbxproj b/ConfidenceDemoApp/ConfidenceDemoApp.xcodeproj/project.pbxproj index cb0c65d5..4663a4e3 100644 --- a/ConfidenceDemoApp/ConfidenceDemoApp.xcodeproj/project.pbxproj +++ b/ConfidenceDemoApp/ConfidenceDemoApp.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 733219BF2BE3C11100747AC2 /* ConfidenceOpenFeature in Frameworks */ = {isa = PBXBuildFile; productRef = 733219BE2BE3C11100747AC2 /* ConfidenceOpenFeature */; }; + 735EADF52CF9B64E007BC42C /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 735EADF42CF9B64E007BC42C /* LoginView.swift */; }; C770C99A2A739FBC00C2AC8C /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C770C9962A739FBC00C2AC8C /* Preview Assets.xcassets */; }; C770C99B2A739FBC00C2AC8C /* ConfidenceDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C770C9972A739FBC00C2AC8C /* ConfidenceDemoApp.swift */; }; C770C99C2A739FBC00C2AC8C /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C770C9982A739FBC00C2AC8C /* ContentView.swift */; }; @@ -35,6 +36,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 735EADF42CF9B64E007BC42C /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; C770C9682A739FA000C2AC8C /* ConfidenceDemoApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ConfidenceDemoApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; C770C9782A739FA100C2AC8C /* ConfidenceDemoAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ConfidenceDemoAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C770C9822A739FA100C2AC8C /* ConfidenceDemoAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ConfidenceDemoAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -103,6 +105,7 @@ C770C9AA2A73A06000C2AC8C /* Info.plist */, C770C9992A739FBC00C2AC8C /* Assets.xcassets */, C770C9972A739FBC00C2AC8C /* ConfidenceDemoApp.swift */, + 735EADF42CF9B64E007BC42C /* LoginView.swift */, C770C9982A739FBC00C2AC8C /* ContentView.swift */, C770C9952A739FBC00C2AC8C /* Preview Content */, ); @@ -285,6 +288,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 735EADF52CF9B64E007BC42C /* LoginView.swift in Sources */, C770C99C2A739FBC00C2AC8C /* ContentView.swift in Sources */, C770C99B2A739FBC00C2AC8C /* ConfidenceDemoApp.swift in Sources */, ); diff --git a/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift b/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift index 6d79beb7..eb5ea8a0 100644 --- a/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift +++ b/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift @@ -1,50 +1,53 @@ import Confidence import SwiftUI -class Status: ObservableObject { - enum State { - case unknown - case ready - case error(Error?) - } - - @Published var state: State = .unknown -} - - @main struct ConfidenceDemoApp: App { - @StateObject private var lifecycleObserver = ConfidenceAppLifecycleProducer() + @AppStorage("loggedUser") private var loggedUser: String? + // @StateObject + private var flaggingState = ExperimentationFlags() + private let confidence = Confidence + .Builder(clientSecret: ProcessInfo.processInfo.environment["CLIENT_SECRET"] ?? "", loggerLevel: .TRACE) + .build() + + init() { + 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)) + } + } var body: some Scene { WindowGroup { - let secret = ProcessInfo.processInfo.environment["CLIENT_SECRET"] ?? "" - let confidence = Confidence.Builder(clientSecret: secret, loggerLevel: .TRACE) - .withContext(initialContext: [ - "targeting_key": ConfidenceValue(string: UUID.init().uuidString), - "user_id": .init(string: "user2") - ]) - .build() - - let status = Status() - - ContentView(confidence: confidence, status: status) - .task { - do { - confidence.track(producer: lifecycleObserver) - try await self.setup(confidence: confidence) - status.state = .ready - } catch { - status.state = .error(error) - print(error.localizedDescription) - } - } + if (loggedUser == nil) { + LoginView(confidence: confidence) + .environmentObject(flaggingState) + } else { + ContentView(confidence: confidence) + .environmentObject(flaggingState) + } } } } -extension ConfidenceDemoApp { - func setup(confidence: Confidence) async throws { - try await confidence.fetchAndActivate() +class ExperimentationFlags: ObservableObject { + @Published var state: State = .notReady + + enum State: Equatable { + case unknown + case notReady + case loading + case ready + case error(CustomError?) + } + + public struct CustomError: Error, Equatable { + let message: String } } diff --git a/ConfidenceDemoApp/ConfidenceDemoApp/ContentView.swift b/ConfidenceDemoApp/ConfidenceDemoApp/ContentView.swift index cfa931f8..07793c96 100644 --- a/ConfidenceDemoApp/ConfidenceDemoApp/ContentView.swift +++ b/ConfidenceDemoApp/ConfidenceDemoApp/ContentView.swift @@ -3,59 +3,51 @@ import Confidence import Combine struct ContentView: View { - @ObservedObject var status: Status - @StateObject var text = DisplayText() - @StateObject var color = FlagColor() + @EnvironmentObject var status: ExperimentationFlags + @AppStorage("loggedUser") private var loggedUser: String? + @State var isLoggingOut: Bool = false + private let color: Color private let confidence: Confidence - init(confidence: Confidence, status: Status) { + init(confidence: Confidence) { self.confidence = confidence - self.status = status + self.color = ContentView.getColor(color: confidence.getValue(key: "swift-demoapp.color", defaultValue: "Gray")) } var body: some View { - if case .ready = status.state { + NavigationStack { VStack { - Image(systemName: "flag") - .imageScale(.large) - .foregroundColor(color.color) - .padding(10) - Text(text.text) - Button("Get remote flag value") { - text.text = confidence.getValue(key: "swift-demoapp.color", defaultValue: "ERROR") - if text.text == "Green" { - color.color = .green - } else if text.text == "Yellow" { - color.color = .yellow - } else { - color.color = .red - } - } - Button("Flush 🚽") { - confidence.flush() - } + Text("Hello World!") + .foregroundStyle(color) } .padding() - } else if case .error(let error) = status.state { - VStack { - Text("Provider Error") - Text(error?.localizedDescription ?? "An unknow error has occured.") - .foregroundColor(.red) + Button("Logout") { + loggedUser = nil + status.state = .loading + Task { + await confidence.removeContext(key: "user_id") + status.state = .ready + } + loggedUser = nil + isLoggingOut = true } - } else { - VStack { - ProgressView() + .navigationDestination(isPresented: $isLoggingOut) { + LoginView(confidence: confidence) } } } -} -class DisplayText: ObservableObject { - @Published var text = "Hello World!" -} - - -class FlagColor: ObservableObject { - @Published var color: Color = .black + private static func getColor(color: String) -> Color { + switch color { + case "Green": + .green + case "Yellow": + .yellow + case "Gray": + .gray + default: + .red + } + } } diff --git a/ConfidenceDemoApp/ConfidenceDemoApp/LoginView.swift b/ConfidenceDemoApp/ConfidenceDemoApp/LoginView.swift new file mode 100644 index 00000000..f0469dd5 --- /dev/null +++ b/ConfidenceDemoApp/ConfidenceDemoApp/LoginView.swift @@ -0,0 +1,54 @@ +import SwiftUI +import Confidence + +struct LoginView: View { + @EnvironmentObject var status: ExperimentationFlags + @AppStorage("loggedUser") private var loggedUser: String? + @State private var loginCompleted = false + @State private var flagsLoaded = false + @State private var loggingIn = false + + private let confidence: Confidence + + init(confidence: Confidence) { + self.confidence = confidence + } + + var body: some View { + NavigationStack { + VStack { + Spacer() + Button("Login as user1") { + status.state = .loading + // Load flags + Task { + try? await Task.sleep(nanoseconds: 5 * 1_000_000_000) // Sleep for 3 seconds + await confidence.putContext(context: ["user_id": .init(string: "user1")]) + status.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 + loggedUser = "user1" + loggingIn = false + loginCompleted = true + } + } + .navigationDestination(isPresented: $loginCompleted) { + ContentView(confidence: confidence) + } + if (loggingIn) { + ProgressView() + } + Spacer() + } + } + } + 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 a794f3d8..4b4f0127 100644 --- a/Sources/Confidence/Confidence.swift +++ b/Sources/Confidence/Confidence.swift @@ -64,7 +64,10 @@ public class Confidence: ConfidenceEventSender { } let savedFlags = try storage.load(defaultValue: FlagResolution.EMPTY) cache = savedFlags - debugLogger?.logFlags(action: "Activate", flag: "") + debugLogger?.logMessage( + message: "[Activating stored cache]", + isWarning: false + ) } } @@ -111,7 +114,6 @@ public class Confidence: ConfidenceEventSender { flags: resolvedFlags.resolvedValues, resolveToken: resolvedFlags.resolveToken ?? "" ) - debugLogger?.logFlags(action: "Fetch", flag: "") try storage.save(data: resolution) } @@ -252,7 +254,7 @@ public class Confidence: ConfidenceEventSender { } contextSubject.value = map debugLogger?.logContext( - action: "PutContext", + action: "PutContextLocal", context: contextSubject.value) } } @@ -270,7 +272,7 @@ public class Confidence: ConfidenceEventSender { do { try await fetchAndActivate() debugLogger?.logContext( - action: "PutContext & FetchAndActivate", + action: "PutContext - Done with FetchAndActivate", context: contextSubject.value) } catch { debugLogger?.logMessage( @@ -296,7 +298,7 @@ public class Confidence: ConfidenceEventSender { do { try await self.fetchAndActivate() debugLogger?.logContext( - action: "PutContext & FetchAndActivate", + action: "PutContext - Done with FetchAndActivate", context: contextSubject.value) } catch { debugLogger?.logMessage( @@ -318,7 +320,7 @@ public class Confidence: ConfidenceEventSender { do { try await self.fetchAndActivate() debugLogger?.logContext( - action: "RemoveContextKey & FetchAndActivate", + action: "RemoveContext - Done with FetchAndActivate", context: contextSubject.value) } catch { debugLogger?.logMessage( diff --git a/Sources/Confidence/DebugLogger.swift b/Sources/Confidence/DebugLogger.swift index 46de6df1..3704c86e 100644 --- a/Sources/Confidence/DebugLogger.swift +++ b/Sources/Confidence/DebugLogger.swift @@ -5,6 +5,7 @@ internal protocol DebugLogger { func logEvent(action: String, event: ConfidenceEvent?) func logMessage(message: String, isWarning: Bool) func logFlags(action: String, flag: String) + func logFlags(action: String, context: ConfidenceStruct) func logContext(action: String, context: ConfidenceStruct) func logResolveDebugURL(flagName: String, context: ConfidenceStruct) } @@ -59,6 +60,10 @@ internal class DebugLoggerImpl: DebugLogger { log(messageLevel: .TRACE, message: "[\(action)] \(flag)") } + func logFlags(action: String, context: ConfidenceStruct) { + log(messageLevel: .TRACE, message: "[\(action)] \(context)") + } + func logContext(action: String, context: ConfidenceStruct) { log(messageLevel: .TRACE, message: "[\(action)] \(context)") }