From 4197d79d240a0398ff1ed021f6289396f09d73c3 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 31 Aug 2022 14:55:30 +0100
Subject: [PATCH] Move app launch logic to AppFeature

---
 Examples/xx-messenger/Package.swift           |   2 -
 .../AppFeature/AppEnvironment+Live.swift      |  32 ++--
 .../Sources/AppFeature/AppFeature.swift       |  99 +++++++++--
 .../Sources/AppFeature/AppView.swift          |  97 +++++++++--
 .../AppFeatureTests/AppFeatureTests.swift     | 160 +++++++++++++++++-
 5 files changed, 333 insertions(+), 57 deletions(-)

diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 5eb7bede..b91c4340 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -69,8 +69,6 @@ let package = Package(
       dependencies: [
         .target(name: "AppCore"),
         .target(name: "HomeFeature"),
-        .target(name: "LaunchFeature"),
-        .target(name: "RegisterFeature"),
         .target(name: "RestoreFeature"),
         .target(name: "WelcomeFeature"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index ff97f803..eec6f199 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -1,7 +1,6 @@
 import AppCore
 import Foundation
 import HomeFeature
-import LaunchFeature
 import RegisterFeature
 import RestoreFeature
 import WelcomeFeature
@@ -17,31 +16,20 @@ extension AppEnvironment {
     let bgQueue = DispatchQueue.global(qos: .background).eraseToAnyScheduler()
 
     return AppEnvironment(
-      launch: {
-        LaunchEnvironment(
-          dbManager: dbManager,
+      dbManager: dbManager,
+      messenger: messenger,
+      mainQueue: mainQueue,
+      bgQueue: bgQueue,
+      welcome: {
+        WelcomeEnvironment(
           messenger: messenger,
           mainQueue: mainQueue,
-          bgQueue: bgQueue,
-          welcome: {
-            WelcomeEnvironment(
-              messenger: messenger,
-              mainQueue: mainQueue,
-              bgQueue: bgQueue
-            )
-          },
-          restore: {
-            RestoreEnvironment()
-          },
-          register: {
-            RegisterEnvironment(
-              messenger: messenger,
-              mainQueue: mainQueue,
-              bgQueue: bgQueue
-            )
-          }
+          bgQueue: bgQueue
         )
       },
+      restore: {
+        RestoreEnvironment()
+      },
       home: {
         HomeEnvironment(
           messenger: messenger,
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift
index 18927c0b..535d69fc 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift
@@ -1,21 +1,33 @@
+import AppCore
+import Combine
 import ComposableArchitecture
 import ComposablePresentation
+import Foundation
 import HomeFeature
-import LaunchFeature
+import RestoreFeature
+import WelcomeFeature
+import XXMessengerClient
 
 struct AppState: Equatable {
   enum Screen: Equatable {
-    case launch(LaunchState)
+    case loading
+    case welcome(WelcomeState)
+    case restore(RestoreState)
     case home(HomeState)
+    case failure(String)
   }
 
-  var screen: Screen = .launch(LaunchState())
+  @BindableState var screen: Screen = .loading
 }
 
 extension AppState.Screen {
-  var asLaunch: LaunchState? {
-    get { (/AppState.Screen.launch).extract(from: self) }
-    set { if let newValue = newValue { self = .launch(newValue) } }
+  var asWelcome: WelcomeState? {
+    get { (/AppState.Screen.welcome).extract(from: self) }
+    set { if let newValue = newValue { self = .welcome(newValue) } }
+  }
+  var asRestore: RestoreState? {
+    get { (/AppState.Screen.restore).extract(from: self) }
+    set { if let state = newValue { self = .restore(state) } }
   }
   var asHome: HomeState? {
     get { (/AppState.Screen.home).extract(from: self) }
@@ -23,19 +35,32 @@ extension AppState.Screen {
   }
 }
 
-enum AppAction: Equatable {
+enum AppAction: Equatable, BindableAction {
+  case start
+  case binding(BindingAction<AppState>)
+  case welcome(WelcomeAction)
+  case restore(RestoreAction)
   case home(HomeAction)
-  case launch(LaunchAction)
 }
 
 struct AppEnvironment {
-  var launch: () -> LaunchEnvironment
+  var dbManager: DBManager
+  var messenger: Messenger
+  var mainQueue: AnySchedulerOf<DispatchQueue>
+  var bgQueue: AnySchedulerOf<DispatchQueue>
+  var welcome: () -> WelcomeEnvironment
+  var restore: () -> RestoreEnvironment
   var home: () -> HomeEnvironment
 }
 
 extension AppEnvironment {
   static let unimplemented = AppEnvironment(
-    launch: { .unimplemented },
+    dbManager: .unimplemented,
+    messenger: .unimplemented,
+    mainQueue: .unimplemented,
+    bgQueue: .unimplemented,
+    welcome: { .unimplemented },
+    restore: { .unimplemented },
     home: { .unimplemented }
   )
 }
@@ -43,20 +68,60 @@ extension AppEnvironment {
 let appReducer = Reducer<AppState, AppAction, AppEnvironment>
 { state, action, env in
   switch action {
-  case .launch(.finished):
-    state.screen = .home(HomeState())
+  case .start, .welcome(.finished), .restore(.finished):
+    state.screen = .loading
+    return .run { subscriber in
+      do {
+        if env.dbManager.hasDB() == false {
+          try env.dbManager.makeDB()
+        }
+
+        if env.messenger.isLoaded() == false {
+          if env.messenger.isCreated() == false {
+            subscriber.send(.set(\.$screen, .welcome(WelcomeState())))
+            subscriber.send(completion: .finished)
+            return AnyCancellable {}
+          }
+          try env.messenger.load()
+        }
+
+        subscriber.send(.set(\.$screen, .home(HomeState())))
+      } catch {
+        subscriber.send(.set(\.$screen, .failure(error.localizedDescription)))
+      }
+      subscriber.send(completion: .finished)
+      return AnyCancellable {}
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
+
+  case .welcome(.restoreTapped):
+    state.screen = .restore(RestoreState())
     return .none
 
-  case .launch(_), .home(_):
+  case .welcome(.failed(let failure)):
+    state.screen = .failure(failure)
+    return .none
+
+  case .binding(_), .welcome(_), .restore(_), .home(_):
     return .none
   }
 }
+.binding()
+.presenting(
+  welcomeReducer,
+  state: .keyPath(\.screen.asWelcome),
+  id: .notNil(),
+  action: /AppAction.welcome,
+  environment: { $0.welcome() }
+)
 .presenting(
-  launchReducer,
-  state: .keyPath(\.screen.asLaunch),
+  restoreReducer,
+  state: .keyPath(\.screen.asRestore),
   id: .notNil(),
-  action: /AppAction.launch,
-  environment: { $0.launch() }
+  action: /AppAction.restore,
+  environment: { $0.restore() }
 )
 .presenting(
   homeReducer,
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppView.swift b/Examples/xx-messenger/Sources/AppFeature/AppView.swift
index 317560a3..2af6d352 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppView.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppView.swift
@@ -1,19 +1,26 @@
 import ComposableArchitecture
-import SwiftUI
 import HomeFeature
-import LaunchFeature
+import RestoreFeature
+import SwiftUI
+import WelcomeFeature
 
 struct AppView: View {
   let store: Store<AppState, AppAction>
 
   enum ViewState: Equatable {
-    case launch
+    case loading
+    case welcome
+    case restore
     case home
+    case failure(String)
 
     init(_ state: AppState) {
       switch state.screen {
-      case .launch(_): self = .launch
+      case .loading: self = .loading
+      case .welcome(_): self = .welcome
+      case .restore(_): self = .restore
       case .home(_): self = .home
+      case .failure(let failure): self = .failure(failure)
       }
     }
   }
@@ -21,20 +28,53 @@ struct AppView: View {
   var body: some View {
     WithViewStore(store.scope(state: ViewState.init)) { viewStore in
       ZStack {
-        SwitchStore(store.scope(state: \.screen)) {
-          CaseLet(
-            state: /AppState.Screen.launch,
-            action: AppAction.launch,
+        switch viewStore.state {
+        case .loading:
+          ProgressView {
+            Text("Loading")
+          }
+          .controlSize(.large)
+          .frame(maxWidth: .infinity, maxHeight: .infinity)
+          .transition(.opacity)
+
+        case .welcome:
+          IfLetStore(
+            store.scope(
+              state: { (/AppState.Screen.welcome).extract(from: $0.screen) },
+              action: AppAction.welcome
+            ),
+            then: { store in
+              WelcomeView(store: store)
+                .frame(maxWidth: .infinity, maxHeight: .infinity)
+                .transition(.asymmetric(
+                  insertion: .move(edge: .trailing),
+                  removal: .opacity
+                ))
+            }
+          )
+
+        case .restore:
+          IfLetStore(
+            store.scope(
+              state: { (/AppState.Screen.restore).extract(from: $0.screen) },
+              action: AppAction.restore
+            ),
             then: { store in
-              LaunchView(store: store)
+              RestoreView(store: store)
                 .frame(maxWidth: .infinity, maxHeight: .infinity)
-                .transition(.opacity)
+                .transition(.asymmetric(
+                  insertion: .move(edge: .trailing),
+                  removal: .opacity
+                ))
             }
           )
 
-          CaseLet(
-            state: /AppState.Screen.home,
-            action: AppAction.home,
+        case .home:
+          IfLetStore(
+            store.scope(
+              state: { (/AppState.Screen.home).extract(from: $0.screen) },
+              action: AppAction.home
+            ),
             then: { store in
               HomeView(store: store)
                 .frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -44,9 +84,40 @@ struct AppView: View {
                 ))
             }
           )
+
+        case .failure(let failure):
+          NavigationView {
+            VStack(spacing: 0) {
+              ScrollView {
+                Text(failure)
+                  .frame(maxWidth: .infinity, alignment: .leading)
+                  .padding()
+              }
+
+              Divider()
+
+              Button {
+                viewStore.send(.start)
+              } label: {
+                Text("Retry")
+                  .frame(maxWidth: .infinity)
+              }
+              .buttonStyle(.borderedProminent)
+              .controlSize(.large)
+              .padding()
+            }
+            .navigationTitle("Error")
+          }
+          .navigationViewStyle(.stack)
+          .frame(maxWidth: .infinity, maxHeight: .infinity)
+          .transition(.asymmetric(
+            insertion: .move(edge: .trailing),
+            removal: .opacity
+          ))
         }
       }
       .animation(.default, value: viewStore.state)
+      .task { viewStore.send(.start) }
     }
   }
 }
diff --git a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift
index 6563da86..166b6325 100644
--- a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift
@@ -1,19 +1,173 @@
 import ComposableArchitecture
 import HomeFeature
+import RestoreFeature
+import WelcomeFeature
 import XCTest
 @testable import AppFeature
 
-@MainActor
 final class AppFeatureTests: XCTestCase {
-  func testLaunchFinished() async throws {
+  func testStartWithoutMessengerCreated() {
     let store = TestStore(
       initialState: AppState(),
       reducer: appReducer,
       environment: .unimplemented
     )
 
-    await store.send(.launch(.finished)) {
+    let mainQueue = DispatchQueue.test
+    let bgQueue = DispatchQueue.test
+    var didMakeDB = 0
+
+    store.environment.mainQueue = mainQueue.eraseToAnyScheduler()
+    store.environment.bgQueue = bgQueue.eraseToAnyScheduler()
+    store.environment.dbManager.hasDB.run = { false }
+    store.environment.dbManager.makeDB.run = { didMakeDB += 1 }
+    store.environment.messenger.isLoaded.run = { false }
+    store.environment.messenger.isCreated.run = { false }
+
+    store.send(.start)
+
+    bgQueue.advance()
+
+    XCTAssertNoDifference(didMakeDB, 1)
+
+    mainQueue.advance()
+
+    store.receive(.set(\.$screen, .welcome(WelcomeState()))) {
+      $0.screen = .welcome(WelcomeState())
+    }
+  }
+
+  func testStartWithMessengerCreated() {
+    let store = TestStore(
+      initialState: AppState(),
+      reducer: appReducer,
+      environment: .unimplemented
+    )
+
+    let mainQueue = DispatchQueue.test
+    let bgQueue = DispatchQueue.test
+    var didMakeDB = 0
+    var messengerDidLoad = 0
+
+    store.environment.mainQueue = mainQueue.eraseToAnyScheduler()
+    store.environment.bgQueue = bgQueue.eraseToAnyScheduler()
+    store.environment.dbManager.hasDB.run = { false }
+    store.environment.dbManager.makeDB.run = { didMakeDB += 1 }
+    store.environment.messenger.isLoaded.run = { false }
+    store.environment.messenger.isCreated.run = { true }
+    store.environment.messenger.load.run = { messengerDidLoad += 1 }
+
+    store.send(.start)
+
+    bgQueue.advance()
+
+    XCTAssertNoDifference(didMakeDB, 1)
+    XCTAssertNoDifference(messengerDidLoad, 1)
+
+    mainQueue.advance()
+
+    store.receive(.set(\.$screen, .home(HomeState()))) {
+      $0.screen = .home(HomeState())
+    }
+  }
+
+  func testWelcomeFinished() {
+    let store = TestStore(
+      initialState: AppState(
+        screen: .welcome(WelcomeState())
+      ),
+      reducer: appReducer,
+      environment: .unimplemented
+    )
+
+    let mainQueue = DispatchQueue.test
+    let bgQueue = DispatchQueue.test
+    var messengerDidLoad = 0
+
+    store.environment.mainQueue = mainQueue.eraseToAnyScheduler()
+    store.environment.bgQueue = bgQueue.eraseToAnyScheduler()
+    store.environment.dbManager.hasDB.run = { true }
+    store.environment.messenger.isLoaded.run = { false }
+    store.environment.messenger.isCreated.run = { true }
+    store.environment.messenger.load.run = { messengerDidLoad += 1 }
+
+    store.send(.welcome(.finished)) {
+      $0.screen = .loading
+    }
+
+    bgQueue.advance()
+
+    XCTAssertNoDifference(messengerDidLoad, 1)
+
+    mainQueue.advance()
+
+    store.receive(.set(\.$screen, .home(HomeState()))) {
+      $0.screen = .home(HomeState())
+    }
+  }
+
+  func testRestoreFinished() {
+    let store = TestStore(
+      initialState: AppState(
+        screen: .restore(RestoreState())
+      ),
+      reducer: appReducer,
+      environment: .unimplemented
+    )
+
+    let mainQueue = DispatchQueue.test
+    let bgQueue = DispatchQueue.test
+    var messengerDidLoad = 0
+
+    store.environment.mainQueue = mainQueue.eraseToAnyScheduler()
+    store.environment.bgQueue = bgQueue.eraseToAnyScheduler()
+    store.environment.dbManager.hasDB.run = { true }
+    store.environment.messenger.isLoaded.run = { false }
+    store.environment.messenger.isCreated.run = { true }
+    store.environment.messenger.load.run = { messengerDidLoad += 1 }
+
+    store.send(.restore(.finished)) {
+      $0.screen = .loading
+    }
+
+    bgQueue.advance()
+
+    XCTAssertNoDifference(messengerDidLoad, 1)
+
+    mainQueue.advance()
+
+    store.receive(.set(\.$screen, .home(HomeState()))) {
       $0.screen = .home(HomeState())
     }
   }
+
+  func testWelcomeRestoreTapped() {
+    let store = TestStore(
+      initialState: AppState(
+        screen: .welcome(WelcomeState())
+      ),
+      reducer: appReducer,
+      environment: .unimplemented
+    )
+
+    store.send(.welcome(.restoreTapped)) {
+      $0.screen = .restore(RestoreState())
+    }
+  }
+
+  func testWelcomeFailed() {
+    let store = TestStore(
+      initialState: AppState(
+        screen: .welcome(WelcomeState())
+      ),
+      reducer: appReducer,
+      environment: .unimplemented
+    )
+
+    let failure = "Something went wrong"
+
+    store.send(.welcome(.failed(failure))) {
+      $0.screen = .failure(failure)
+    }
+  }
 }
-- 
GitLab