Skip to content

Commit

Permalink
feat: Demo App emulates login scenarios
Browse files Browse the repository at this point in the history
  • Loading branch information
fabriziodemaria committed Dec 5, 2024
1 parent 2b4dc68 commit 64c049f
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 80 deletions.
1 change: 0 additions & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ analyzer_rules:

opt_in_rules:
- array_init
- attributes
- closure_end_indentation
- closure_spacing
- collection_alignment
Expand Down
4 changes: 4 additions & 0 deletions ConfidenceDemoApp/ConfidenceDemoApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -35,6 +36,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
735EADF42CF9B64E007BC42C /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
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; };
Expand Down Expand Up @@ -103,6 +105,7 @@
C770C9AA2A73A06000C2AC8C /* Info.plist */,
C770C9992A739FBC00C2AC8C /* Assets.xcassets */,
C770C9972A739FBC00C2AC8C /* ConfidenceDemoApp.swift */,
735EADF42CF9B64E007BC42C /* LoginView.swift */,
C770C9982A739FBC00C2AC8C /* ContentView.swift */,
C770C9952A739FBC00C2AC8C /* Preview Content */,
);
Expand Down Expand Up @@ -285,6 +288,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
735EADF52CF9B64E007BC42C /* LoginView.swift in Sources */,
C770C99C2A739FBC00C2AC8C /* ContentView.swift in Sources */,
C770C99B2A739FBC00C2AC8C /* ConfidenceDemoApp.swift in Sources */,
);
Expand Down
110 changes: 75 additions & 35 deletions ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift
Original file line number Diff line number Diff line change
@@ -1,50 +1,90 @@
import Confidence
import SwiftUI

class Status: ObservableObject {
enum State {
case unknown
case ready
case error(Error?)
}
@main
struct ConfidenceDemoApp: App {
@AppStorage("loggedUser")
private var loggedUser: String?
@AppStorage("appVersion")
private var appVersion = 0

@Published var state: State = .unknown
}
private let confidence: Confidence
private let flaggingState = ExperimentationFlags()
private let secret = ProcessInfo.processInfo.environment["CLIENT_SECRET"] ?? "<Empty Secret>"

init() {
@AppStorage("appVersion") var appVersion = 0
@AppStorage("loggedUser") var loggedUser: String?
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)
}

@main
struct ConfidenceDemoApp: App {
@StateObject private var lifecycleObserver = ConfidenceAppLifecycleProducer()
confidence = Confidence
.Builder(clientSecret: secret, loggerLevel: .TRACE)
.withContext(initialContext: context)
.build()
do {
// NOTE: here we are activating all the flag values from storage, regardless of how `context` looks now
try confidence.activate()
} catch {
flaggingState.state = .error(ExperimentationFlags.CustomError(message: error.localizedDescription))
}
// flaggingState.color is set here at startup and remains immutable until a user logs out
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 {
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)
}
}
Text("Client secret: \(secret)")
.font(.caption)
if loggedUser == nil {
LoginView(confidence: confidence)
.environmentObject(flaggingState)
} else {
ContentView(confidence: confidence)
.environmentObject(flaggingState)
}
}
}

private func updateConfidence() {
Task {
do {
flaggingState.state = .loading
try await Task.sleep(nanoseconds: 2 * 1_000_000_000) // simulating slow network
// The flags in storage are refreshed for the current `context`, and activated
// After this line, fresh (and potentially new) flags values can be accessed
try await confidence.fetchAndActivate()
flaggingState.state = .ready
} catch {
flaggingState.state = .error(ExperimentationFlags.CustomError(message: error.localizedDescription))
}
}
}
}

extension ConfidenceDemoApp {
func setup(confidence: Confidence) async throws {
try await confidence.fetchAndActivate()
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 {
case unknown
case notReady
case loading
case ready
case error(CustomError?)
}

public struct CustomError: Error, Equatable {
let message: String
}
}
127 changes: 89 additions & 38 deletions ConfidenceDemoApp/ConfidenceDemoApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,59 +3,110 @@ import Confidence
import Combine

struct ContentView: View {
@ObservedObject var status: Status
@StateObject var text = DisplayText()
@StateObject var color = FlagColor()
@EnvironmentObject
var flaggingState: ExperimentationFlags
@AppStorage("loggedUser")
private var loggedUser: String?
@State
private var isLoggingOut = false
@State
private var loggedOut = false
@State
private var textColor = Color.red

private let confidence: Confidence

init(confidence: Confidence, status: Status) {
init(confidence: Confidence, color: Color? = nil) {
self.confidence = confidence
self.status = status
}

var body: some View {
if case .ready = status.state {
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
NavigationStack {
if flaggingState.state == .loading && !isLoggingOut {
VStack {
Spacer()
Text("Login successful: \(loggedUser ?? "?")")
Text("We are preparing your experience...")
ProgressView()
}
} else {
if flaggingState.state == .ready {
VStack {
Spacer()
if let user = loggedUser {
let eval = confidence.getEvaluation(key: "swift-demoapp.color", defaultValue: "Gray")
VStack {
Text("Hello \(user)")
.font(.largeTitle)
.foregroundStyle(ContentView.getColor(color: eval.value))
.padding()
Text("This text only appears after a successful flag fetching")
.font(.caption)
.foregroundStyle(ContentView.getColor(color: eval.value))
.padding()
}
}
}
}
Button("Flush 🚽") {
confidence.flush()
NavigationLink(destination: AboutPage()) {
Text("Navigate")
}
Button("Logout") {
isLoggingOut = true
loggedUser = nil
flaggingState.state = .loading
flaggingState.color = .gray
Task {
await confidence.removeContext(key: "user_id")
flaggingState.state = .ready
}
loggedOut = true
}
.navigationDestination(isPresented: $loggedOut) {
LoginView(confidence: confidence)
}
Spacer()
}
.padding()
} else if case .error(let error) = status.state {
VStack {
Text("Provider Error")
Text(error?.localizedDescription ?? "An unknow error has occured.")
.foregroundColor(.red)
}
} else {
VStack {
ProgressView()
ZStack {
VStack {
Spacer()
Text("This text color is set on onAppear, doesn't wait for flag fetch")
.font(.caption)
.foregroundStyle(textColor)
Text("This text color dynamically changes on each flags fetch")
.font(.caption)
.foregroundStyle(ContentView.getColor(
color: confidence.getValue(
key: "swift-demoapp.color",
defaultValue: "Gray")))
Text("This text color is fixed from app start, doesn't react on flag fetches")
.font(.caption)
.foregroundStyle(flaggingState.color)
}
}.onAppear {
let eval = confidence.getEvaluation(key: "swift-demoapp.color", defaultValue: "Gray")
print(">> Evaluation reason: \(eval)")
textColor = ContentView.getColor(color: eval.value)
}
}
}
}

class DisplayText: ObservableObject {
@Published var text = "Hello World!"
static func getColor(color: String) -> Color {
switch color {
case "Green":
return .green
case "Yellow":
return .yellow
case "Gray":
return .gray
default:
return .red
}
}
}


class FlagColor: ObservableObject {
@Published var color: Color = .black
struct AboutPage: View {
var body: some View {
Text("Mock Page")
}
}
68 changes: 68 additions & 0 deletions ConfidenceDemoApp/ConfidenceDemoApp/LoginView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import SwiftUI
import Confidence

struct LoginView: View {
@EnvironmentObject
var flaggingState: 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") {
do {
try confidence.activate()
} catch {
flaggingState.state = .error(
ExperimentationFlags.CustomError(message: error.localizedDescription))
}

flaggingState.color = ContentView.getColor(
color: confidence.getValue(key: "swift-demoapp.color", defaultValue: "Gray")
)

// Simulating a module that handles feature flagging state during login
Task {
flaggingState.state = .loading
try? await Task.sleep(nanoseconds: 5 * 1_000_000_000) // simulating network delay
// putContext adds the user_id field to the evaluation context and fetches values for it
await confidence.putContext(context: ["user_id": .init(string: "user1")])
flaggingState.state = .ready
}

// Simulating a module that handles the actual login mechanism for a user
Task {
loggingIn = true
try? await Task.sleep(nanoseconds: 1 * 1_000_000_000) // simulating network delay
loggedUser = "user1"
loggingIn = false
loginCompleted = true
}
}
.navigationDestination(isPresented: $loginCompleted) {
ContentView(confidence: confidence)
}

if loggingIn {
ProgressView()
}

Spacer()
}
}
}
}
Loading

0 comments on commit 64c049f

Please sign in to comment.