From 717b8588ea4400c89c463cc768110d60bf296b22 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 1 Jun 2022 13:58:31 +0200
Subject: [PATCH] Setup app, landing, and session features

---
 .../xcschemes/LandingFeature.xcscheme         |  78 +++++++++++
 .../xcschemes/SessionFeature.xcscheme         |  78 +++++++++++
 .../xcschemes/example-app.xcscheme            | 126 ++++++++++++++++++
 Example/ExampleApp/Package.swift              |  42 +++++-
 .../ExampleApp/Sources/AppFeature/App.swift   |  18 ++-
 .../Sources/AppFeature/AppFeature.swift       |  82 ++++++++++++
 .../Sources/AppFeature/AppView.swift          |  66 ++++++++-
 .../LandingFeature/LandingFeature.swift       |  27 ++++
 .../Sources/LandingFeature/LandingView.swift  |  38 ++++++
 .../SessionFeature/SessionFeature.swift       |  27 ++++
 .../Sources/SessionFeature/SessionView.swift  |  35 +++++
 .../AppFeatureTests/AppFeatureTests.swift     |  11 +-
 .../LandingFeatureTests.swift                 |  15 +++
 .../SessionFeatureTests.swift                 |  15 +++
 14 files changed, 651 insertions(+), 7 deletions(-)
 create mode 100644 Example/ExampleApp/.swiftpm/xcode/xcshareddata/xcschemes/LandingFeature.xcscheme
 create mode 100644 Example/ExampleApp/.swiftpm/xcode/xcshareddata/xcschemes/SessionFeature.xcscheme
 create mode 100644 Example/ExampleApp/.swiftpm/xcode/xcshareddata/xcschemes/example-app.xcscheme
 create mode 100644 Example/ExampleApp/Sources/AppFeature/AppFeature.swift
 create mode 100644 Example/ExampleApp/Sources/LandingFeature/LandingFeature.swift
 create mode 100644 Example/ExampleApp/Sources/LandingFeature/LandingView.swift
 create mode 100644 Example/ExampleApp/Sources/SessionFeature/SessionFeature.swift
 create mode 100644 Example/ExampleApp/Sources/SessionFeature/SessionView.swift
 create mode 100644 Example/ExampleApp/Tests/LandingFeatureTests/LandingFeatureTests.swift
 create mode 100644 Example/ExampleApp/Tests/SessionFeatureTests/SessionFeatureTests.swift

diff --git a/Example/ExampleApp/.swiftpm/xcode/xcshareddata/xcschemes/LandingFeature.xcscheme b/Example/ExampleApp/.swiftpm/xcode/xcshareddata/xcschemes/LandingFeature.xcscheme
new file mode 100644
index 00000000..7d203fde
--- /dev/null
+++ b/Example/ExampleApp/.swiftpm/xcode/xcshareddata/xcschemes/LandingFeature.xcscheme
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1340"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "LandingFeature"
+               BuildableName = "LandingFeature"
+               BlueprintName = "LandingFeature"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      codeCoverageEnabled = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "LandingFeatureTests"
+               BuildableName = "LandingFeatureTests"
+               BlueprintName = "LandingFeatureTests"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "LandingFeature"
+            BuildableName = "LandingFeature"
+            BlueprintName = "LandingFeature"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/Example/ExampleApp/.swiftpm/xcode/xcshareddata/xcschemes/SessionFeature.xcscheme b/Example/ExampleApp/.swiftpm/xcode/xcshareddata/xcschemes/SessionFeature.xcscheme
new file mode 100644
index 00000000..9361c555
--- /dev/null
+++ b/Example/ExampleApp/.swiftpm/xcode/xcshareddata/xcschemes/SessionFeature.xcscheme
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1340"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "SessionFeature"
+               BuildableName = "SessionFeature"
+               BlueprintName = "SessionFeature"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      codeCoverageEnabled = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "SessionFeatureTests"
+               BuildableName = "SessionFeatureTests"
+               BlueprintName = "SessionFeatureTests"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "SessionFeature"
+            BuildableName = "SessionFeature"
+            BlueprintName = "SessionFeature"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/Example/ExampleApp/.swiftpm/xcode/xcshareddata/xcschemes/example-app.xcscheme b/Example/ExampleApp/.swiftpm/xcode/xcshareddata/xcschemes/example-app.xcscheme
new file mode 100644
index 00000000..6901d433
--- /dev/null
+++ b/Example/ExampleApp/.swiftpm/xcode/xcshareddata/xcschemes/example-app.xcscheme
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1340"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "AppFeature"
+               BuildableName = "AppFeature"
+               BlueprintName = "AppFeature"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </BuildActionEntry>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "LandingFeature"
+               BuildableName = "LandingFeature"
+               BlueprintName = "LandingFeature"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </BuildActionEntry>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "SessionFeature"
+               BuildableName = "SessionFeature"
+               BlueprintName = "SessionFeature"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      codeCoverageEnabled = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "AppFeatureTests"
+               BuildableName = "AppFeatureTests"
+               BlueprintName = "AppFeatureTests"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "LandingFeatureTests"
+               BuildableName = "LandingFeatureTests"
+               BlueprintName = "LandingFeatureTests"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "SessionFeatureTests"
+               BuildableName = "SessionFeatureTests"
+               BlueprintName = "SessionFeatureTests"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "AppFeature"
+            BuildableName = "AppFeature"
+            BlueprintName = "AppFeature"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/Example/ExampleApp/Package.swift b/Example/ExampleApp/Package.swift
index 3e3cec1e..409928d4 100644
--- a/Example/ExampleApp/Package.swift
+++ b/Example/ExampleApp/Package.swift
@@ -3,7 +3,7 @@
 import PackageDescription
 
 let package = Package(
-  name: "ExampleApp",
+  name: "example-app",
   platforms: [
     .iOS(.v15),
   ],
@@ -12,6 +12,14 @@ let package = Package(
       name: "AppFeature",
       targets: ["AppFeature"]
     ),
+    .library(
+      name: "LandingFeature",
+      targets: ["LandingFeature"]
+    ),
+    .library(
+      name: "SessionFeature",
+      targets: ["SessionFeature"]
+    ),
   ],
   dependencies: [
     .package(path: "../../"), // elixxir-dapps-sdk-swift
@@ -28,6 +36,8 @@ let package = Package(
     .target(
       name: "AppFeature",
       dependencies: [
+        .target(name: "LandingFeature"),
+        .target(name: "SessionFeature"),
         .product(
           name: "ElixxirDAppsSDK",
           package: "elixxir-dapps-sdk-swift"
@@ -48,5 +58,35 @@ let package = Package(
         .target(name: "AppFeature"),
       ]
     ),
+    .target(
+      name: "LandingFeature",
+      dependencies: [
+        .product(
+          name: "ComposableArchitecture",
+          package: "swift-composable-architecture"
+        ),
+      ]
+    ),
+    .testTarget(
+      name: "LandingFeatureTests",
+      dependencies: [
+        .target(name: "LandingFeature"),
+      ]
+    ),
+    .target(
+      name: "SessionFeature",
+      dependencies: [
+        .product(
+          name: "ComposableArchitecture",
+          package: "swift-composable-architecture"
+        ),
+      ]
+    ),
+    .testTarget(
+      name: "SessionFeatureTests",
+      dependencies: [
+        .target(name: "SessionFeature"),
+      ]
+    ),
   ]
 )
diff --git a/Example/ExampleApp/Sources/AppFeature/App.swift b/Example/ExampleApp/Sources/AppFeature/App.swift
index 47af3dbc..0374fa69 100644
--- a/Example/ExampleApp/Sources/AppFeature/App.swift
+++ b/Example/ExampleApp/Sources/AppFeature/App.swift
@@ -1,10 +1,26 @@
+import ComposableArchitecture
+import LandingFeature
+import SessionFeature
 import SwiftUI
 
 @main
 struct App: SwiftUI.App {
   var body: some Scene {
     WindowGroup {
-      AppView()
+      AppView(store: Store(
+        initialState: AppState(),
+        reducer: appReducer,
+        environment: .live()
+      ))
     }
   }
 }
+
+extension AppEnvironment {
+  static func live() -> AppEnvironment {
+    AppEnvironment(
+      landing: LandingEnvironment(),
+      session: SessionEnvironment()
+    )
+  }
+}
diff --git a/Example/ExampleApp/Sources/AppFeature/AppFeature.swift b/Example/ExampleApp/Sources/AppFeature/AppFeature.swift
new file mode 100644
index 00000000..07d6812a
--- /dev/null
+++ b/Example/ExampleApp/Sources/AppFeature/AppFeature.swift
@@ -0,0 +1,82 @@
+import ComposableArchitecture
+import ComposablePresentation
+import LandingFeature
+import SessionFeature
+
+struct AppState: Equatable {
+  enum Scene: Equatable {
+    case landing(LandingState)
+    case session(SessionState)
+  }
+
+  var scene: Scene = .landing(LandingState())
+}
+
+extension AppState.Scene {
+  var asLanding: LandingState? {
+    get {
+      guard case .landing(let state) = self else { return nil }
+      return state
+    }
+    set {
+      guard let newValue = newValue else { return }
+      self = .landing(newValue)
+    }
+  }
+
+  var asSession: SessionState? {
+    get {
+      guard case .session(let state) = self else { return nil }
+      return state
+    }
+    set {
+      guard let newValue = newValue else { return }
+      self = .session(newValue)
+    }
+  }
+}
+
+enum AppAction: Equatable {
+  case viewDidLoad
+  case landing(LandingAction)
+  case session(SessionAction)
+}
+
+struct AppEnvironment {
+  var landing: LandingEnvironment
+  var session: SessionEnvironment
+}
+
+let appReducer = Reducer<AppState, AppAction, AppEnvironment>
+{ state, action, env in
+  switch action {
+  case .viewDidLoad:
+    return .none
+
+  case .landing(_), .session(_):
+    return .none
+  }
+}
+.presenting(
+  landingReducer,
+  state: .keyPath(\.scene.asLanding),
+  id: .notNil(),
+  action: /AppAction.landing,
+  environment: \.landing
+)
+.presenting(
+  sessionReducer,
+  state: .keyPath(\.scene.asSession),
+  id: .notNil(),
+  action: /AppAction.session,
+  environment: \.session
+)
+
+#if DEBUG
+extension AppEnvironment {
+  static let failing = AppEnvironment(
+    landing: .failing,
+    session: .failing
+  )
+}
+#endif
diff --git a/Example/ExampleApp/Sources/AppFeature/AppView.swift b/Example/ExampleApp/Sources/AppFeature/AppView.swift
index ac856328..f2af2d1a 100644
--- a/Example/ExampleApp/Sources/AppFeature/AppView.swift
+++ b/Example/ExampleApp/Sources/AppFeature/AppView.swift
@@ -1,16 +1,76 @@
+import ComposableArchitecture
+import LandingFeature
+import SessionFeature
 import SwiftUI
 
 struct AppView: View {
+  let store: Store<AppState, AppAction>
+
+  struct ViewState: Equatable {
+    enum Scene: Equatable {
+      case landing
+      case session
+    }
+
+    let scene: Scene
+
+    init(state: AppState) {
+      switch state.scene {
+      case .landing(_):
+        self.scene = .landing
+
+      case .session(_):
+        self.scene = .session
+      }
+    }
+  }
+
   var body: some View {
-    Text("AppView")
-      .padding()
+    WithViewStore(store.scope(state: ViewState.init)) { viewStore in
+      ZStack {
+        SwitchStore(store.scope(state: \.scene)) {
+          CaseLet(
+            state: /AppState.Scene.landing,
+            action: AppAction.landing,
+            then: { store in
+              NavigationView {
+                LandingView(store: store)
+              }
+              .navigationViewStyle(.stack)
+              .transition(.move(edge: .leading))
+            }
+          )
+
+          CaseLet(
+            state: /AppState.Scene.session,
+            action: AppAction.session,
+            then: { store in
+              NavigationView {
+                SessionView(store: store)
+              }
+              .navigationViewStyle(.stack)
+              .transition(.move(edge: .trailing))
+            }
+          )
+        }
+        .frame(maxWidth: .infinity, maxHeight: .infinity)
+      }
+      .animation(.default, value: viewStore.scene)
+      .task {
+        viewStore.send(.viewDidLoad)
+      }
+    }
   }
 }
 
 #if DEBUG
 struct AppView_Previews: PreviewProvider {
   static var previews: some View {
-    AppView()
+    AppView(store: Store(
+      initialState: AppState(),
+      reducer: .empty,
+      environment: ()
+    ))
   }
 }
 #endif
diff --git a/Example/ExampleApp/Sources/LandingFeature/LandingFeature.swift b/Example/ExampleApp/Sources/LandingFeature/LandingFeature.swift
new file mode 100644
index 00000000..b145b250
--- /dev/null
+++ b/Example/ExampleApp/Sources/LandingFeature/LandingFeature.swift
@@ -0,0 +1,27 @@
+import ComposableArchitecture
+
+public struct LandingState: Equatable {
+  public init() {}
+}
+
+public enum LandingAction: Equatable {
+  case viewDidLoad
+}
+
+public struct LandingEnvironment {
+  public init() {}
+}
+
+public let landingReducer = Reducer<LandingState, LandingAction, LandingEnvironment>
+{ state, action, env in
+  switch action {
+  case .viewDidLoad:
+    return .none
+  }
+}
+
+#if DEBUG
+extension LandingEnvironment {
+  public static let failing = LandingEnvironment()
+}
+#endif
diff --git a/Example/ExampleApp/Sources/LandingFeature/LandingView.swift b/Example/ExampleApp/Sources/LandingFeature/LandingView.swift
new file mode 100644
index 00000000..5ebc5e89
--- /dev/null
+++ b/Example/ExampleApp/Sources/LandingFeature/LandingView.swift
@@ -0,0 +1,38 @@
+import ComposableArchitecture
+import SwiftUI
+
+public struct LandingView: View {
+  public init(store: Store<LandingState, LandingAction>) {
+    self.store = store
+  }
+
+  let store: Store<LandingState, LandingAction>
+
+  struct ViewState: Equatable {
+    init(state: LandingState) {}
+  }
+
+  public var body: some View {
+    WithViewStore(store.scope(state: ViewState.init)) { viewStore in
+      Text("LandingView")
+        .task {
+          viewStore.send(.viewDidLoad)
+        }
+    }
+  }
+}
+
+#if DEBUG
+public struct LandingView_Previews: PreviewProvider {
+  public static var previews: some View {
+    NavigationView {
+      LandingView(store: .init(
+        initialState: .init(),
+        reducer: .empty,
+        environment: ()
+      ))
+    }
+    .navigationViewStyle(.stack)
+  }
+}
+#endif
diff --git a/Example/ExampleApp/Sources/SessionFeature/SessionFeature.swift b/Example/ExampleApp/Sources/SessionFeature/SessionFeature.swift
new file mode 100644
index 00000000..3b7dd16f
--- /dev/null
+++ b/Example/ExampleApp/Sources/SessionFeature/SessionFeature.swift
@@ -0,0 +1,27 @@
+import ComposableArchitecture
+
+public struct SessionState: Equatable {
+  public init() {}
+}
+
+public enum SessionAction: Equatable {
+  case viewDidLoad
+}
+
+public struct SessionEnvironment {
+  public init() {}
+}
+
+public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironment>
+{ state, action, env in
+  switch action {
+  case .viewDidLoad:
+    return .none
+  }
+}
+
+#if DEBUG
+extension SessionEnvironment {
+  public static let failing = SessionEnvironment()
+}
+#endif
diff --git a/Example/ExampleApp/Sources/SessionFeature/SessionView.swift b/Example/ExampleApp/Sources/SessionFeature/SessionView.swift
new file mode 100644
index 00000000..781ee440
--- /dev/null
+++ b/Example/ExampleApp/Sources/SessionFeature/SessionView.swift
@@ -0,0 +1,35 @@
+import ComposableArchitecture
+import SwiftUI
+
+public struct SessionView: View {
+  public init(store: Store<SessionState, SessionAction>) {
+    self.store = store
+  }
+
+  let store: Store<SessionState, SessionAction>
+
+  struct ViewState: Equatable {
+    init(state: SessionState) {}
+  }
+
+  public var body: some View {
+    WithViewStore(store.scope(state: ViewState.init)) { viewStore in
+      Text("SessionView")
+        .task {
+          viewStore.send(.viewDidLoad)
+        }
+    }
+  }
+}
+
+#if DEBUG
+public struct SessionView_Previews: PreviewProvider {
+  public static var previews: some View {
+    SessionView(store: .init(
+      initialState: .init(),
+      reducer: .empty,
+      environment: ()
+    ))
+  }
+}
+#endif
diff --git a/Example/ExampleApp/Tests/AppFeatureTests/AppFeatureTests.swift b/Example/ExampleApp/Tests/AppFeatureTests/AppFeatureTests.swift
index 23825304..2fe7038c 100644
--- a/Example/ExampleApp/Tests/AppFeatureTests/AppFeatureTests.swift
+++ b/Example/ExampleApp/Tests/AppFeatureTests/AppFeatureTests.swift
@@ -1,8 +1,15 @@
+import ComposableArchitecture
 import XCTest
 @testable import AppFeature
 
 final class AppFeatureTests: XCTestCase {
-  func testExample() throws {
-    XCTAssert(true)
+  func testViewDidLoad() throws {
+    let store = TestStore(
+      initialState: AppState(),
+      reducer: appReducer,
+      environment: .failing
+    )
+
+    store.send(.viewDidLoad)
   }
 }
diff --git a/Example/ExampleApp/Tests/LandingFeatureTests/LandingFeatureTests.swift b/Example/ExampleApp/Tests/LandingFeatureTests/LandingFeatureTests.swift
new file mode 100644
index 00000000..389863f6
--- /dev/null
+++ b/Example/ExampleApp/Tests/LandingFeatureTests/LandingFeatureTests.swift
@@ -0,0 +1,15 @@
+import ComposableArchitecture
+import XCTest
+@testable import LandingFeature
+
+final class LandingFeatureTests: XCTestCase {
+  func testViewDidLoad() throws {
+    let store = TestStore(
+      initialState: LandingState(),
+      reducer: landingReducer,
+      environment: .failing
+    )
+
+    store.send(.viewDidLoad)
+  }
+}
diff --git a/Example/ExampleApp/Tests/SessionFeatureTests/SessionFeatureTests.swift b/Example/ExampleApp/Tests/SessionFeatureTests/SessionFeatureTests.swift
new file mode 100644
index 00000000..5ed11e32
--- /dev/null
+++ b/Example/ExampleApp/Tests/SessionFeatureTests/SessionFeatureTests.swift
@@ -0,0 +1,15 @@
+import ComposableArchitecture
+import XCTest
+@testable import SessionFeature
+
+final class SessionFeatureTests: XCTestCase {
+  func testViewDidLoad() throws {
+    let store = TestStore(
+      initialState: SessionState(),
+      reducer: sessionReducer,
+      environment: .failing
+    )
+
+    store.send(.viewDidLoad)
+  }
+}
-- 
GitLab