From c506cef20c8511607dd423150a8ebbfbbf1faf52 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 8 Jun 2022 12:40:21 +0200
Subject: [PATCH 01/13] Add MyIdentityFeature library to example app

---
 .../xcschemes/MyIdentityFeature.xcscheme      | 78 +++++++++++++++++++
 .../xcschemes/example-app.xcscheme            | 24 ++++++
 Example/example-app/Package.swift             | 21 +++++
 .../MyIdentityFeature/MyIdentityFeature.swift | 19 +++++
 .../MyIdentityFeature/MyIdentityView.swift    | 35 +++++++++
 .../MyIdentityFeatureTests.swift              |  9 +++
 6 files changed, 186 insertions(+)
 create mode 100644 Example/example-app/.swiftpm/xcode/xcshareddata/xcschemes/MyIdentityFeature.xcscheme
 create mode 100644 Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
 create mode 100644 Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift
 create mode 100644 Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift

diff --git a/Example/example-app/.swiftpm/xcode/xcshareddata/xcschemes/MyIdentityFeature.xcscheme b/Example/example-app/.swiftpm/xcode/xcshareddata/xcschemes/MyIdentityFeature.xcscheme
new file mode 100644
index 00000000..e69b1d44
--- /dev/null
+++ b/Example/example-app/.swiftpm/xcode/xcshareddata/xcschemes/MyIdentityFeature.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 = "MyIdentityFeature"
+               BuildableName = "MyIdentityFeature"
+               BlueprintName = "MyIdentityFeature"
+               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 = "MyIdentityFeatureTests"
+               BuildableName = "MyIdentityFeatureTests"
+               BlueprintName = "MyIdentityFeatureTests"
+               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 = "MyIdentityFeature"
+            BuildableName = "MyIdentityFeature"
+            BlueprintName = "MyIdentityFeature"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/Example/example-app/.swiftpm/xcode/xcshareddata/xcschemes/example-app.xcscheme b/Example/example-app/.swiftpm/xcode/xcshareddata/xcschemes/example-app.xcscheme
index ac213623..1a4d13ff 100644
--- a/Example/example-app/.swiftpm/xcode/xcshareddata/xcschemes/example-app.xcscheme
+++ b/Example/example-app/.swiftpm/xcode/xcshareddata/xcschemes/example-app.xcscheme
@@ -48,6 +48,20 @@
                ReferencedContainer = "container:">
             </BuildableReference>
          </BuildActionEntry>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "MyIdentityFeature"
+               BuildableName = "MyIdentityFeature"
+               BlueprintName = "MyIdentityFeature"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </BuildActionEntry>
          <BuildActionEntry
             buildForTesting = "YES"
             buildForRunning = "YES"
@@ -101,6 +115,16 @@
                ReferencedContainer = "container:">
             </BuildableReference>
          </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "MyIdentityFeatureTests"
+               BuildableName = "MyIdentityFeatureTests"
+               BlueprintName = "MyIdentityFeatureTests"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </TestableReference>
          <TestableReference
             skipped = "NO">
             <BuildableReference
diff --git a/Example/example-app/Package.swift b/Example/example-app/Package.swift
index 55d54626..cb8aead7 100644
--- a/Example/example-app/Package.swift
+++ b/Example/example-app/Package.swift
@@ -32,6 +32,10 @@ let package = Package(
       name: "LandingFeature",
       targets: ["LandingFeature"]
     ),
+    .library(
+      name: "MyIdentityFeature",
+      targets: ["MyIdentityFeature"]
+    ),
     .library(
       name: "SessionFeature",
       targets: ["SessionFeature"]
@@ -132,6 +136,23 @@ let package = Package(
       ],
       swiftSettings: swiftSettings
     ),
+    .target(
+      name: "MyIdentityFeature",
+      dependencies: [
+        .product(
+          name: "ComposableArchitecture",
+          package: "swift-composable-architecture"
+        ),
+      ],
+      swiftSettings: swiftSettings
+    ),
+    .testTarget(
+      name: "MyIdentityFeatureTests",
+      dependencies: [
+        .target(name: "MyIdentityFeature"),
+      ],
+      swiftSettings: swiftSettings
+    ),
     .target(
       name: "SessionFeature",
       dependencies: [
diff --git a/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift b/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
new file mode 100644
index 00000000..a9b97361
--- /dev/null
+++ b/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
@@ -0,0 +1,19 @@
+import ComposableArchitecture
+
+public struct MyIdentityState: Equatable {
+  public init() {}
+}
+
+public enum MyIdentityAction: Equatable {}
+
+public struct MyIdentityEnvironment {
+  public init() {}
+}
+
+public let myIdentityReducer = Reducer<MyIdentityState, MyIdentityAction, MyIdentityEnvironment>.empty
+
+#if DEBUG
+extension MyIdentityEnvironment {
+  public static let failing = MyIdentityEnvironment()
+}
+#endif
diff --git a/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift b/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift
new file mode 100644
index 00000000..4061024b
--- /dev/null
+++ b/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift
@@ -0,0 +1,35 @@
+import ComposableArchitecture
+import SwiftUI
+
+public struct MyIdentityView: View {
+  public init(store: Store<MyIdentityState, MyIdentityAction>) {
+    self.store = store
+  }
+
+  let store: Store<MyIdentityState, MyIdentityAction>
+
+  struct ViewState: Equatable {
+    init(state: MyIdentityState) {}
+  }
+
+  public var body: some View {
+    WithViewStore(store.scope(state: ViewState.init)) { viewStore in
+      Text("MyIdentityView")
+    }
+  }
+}
+
+#if DEBUG
+public struct MyIdentityView_Previews: PreviewProvider {
+  public static var previews: some View {
+    NavigationView {
+      MyIdentityView(store: .init(
+        initialState: .init(),
+        reducer: .empty,
+        environment: ()
+      ))
+    }
+    .navigationViewStyle(.stack)
+  }
+}
+#endif
diff --git a/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift b/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift
new file mode 100644
index 00000000..f4a6a410
--- /dev/null
+++ b/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift
@@ -0,0 +1,9 @@
+import ComposableArchitecture
+import XCTest
+@testable import MyIdentityFeature
+
+final class MyIdentityFeatureTests: XCTestCase {
+  func testExample() {
+    XCTAssert(true)
+  }
+}
-- 
GitLab


From 1f170097b2f6621ea6b1df04762d780a36282f3d Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 8 Jun 2022 12:57:11 +0200
Subject: [PATCH 02/13] Present MyIdentityView from SessionView

---
 Example/example-app/Package.swift             |  2 +
 .../example-app/Sources/AppFeature/App.swift  |  5 ++-
 .../MyIdentityFeature/MyIdentityFeature.swift |  8 +++-
 .../MyIdentityFeature/MyIdentityView.swift    |  2 +-
 .../SessionFeature/SessionFeature.swift       | 40 +++++++++++++++++--
 .../Sources/SessionFeature/SessionView.swift  | 25 ++++++++++++
 .../SessionFeatureTests.swift                 | 22 ++++++++++
 7 files changed, 97 insertions(+), 7 deletions(-)

diff --git a/Example/example-app/Package.swift b/Example/example-app/Package.swift
index cb8aead7..0c0ef222 100644
--- a/Example/example-app/Package.swift
+++ b/Example/example-app/Package.swift
@@ -62,6 +62,7 @@ let package = Package(
       dependencies: [
         .target(name: "ErrorFeature"),
         .target(name: "LandingFeature"),
+        .target(name: "MyIdentityFeature"),
         .target(name: "SessionFeature"),
         .product(
           name: "ElixxirDAppsSDK",
@@ -157,6 +158,7 @@ let package = Package(
       name: "SessionFeature",
       dependencies: [
         .target(name: "ErrorFeature"),
+        .target(name: "MyIdentityFeature"),
         .product(
           name: "ComposableArchitecture",
           package: "swift-composable-architecture"
diff --git a/Example/example-app/Sources/AppFeature/App.swift b/Example/example-app/Sources/AppFeature/App.swift
index 341cb46c..06477b12 100644
--- a/Example/example-app/Sources/AppFeature/App.swift
+++ b/Example/example-app/Sources/AppFeature/App.swift
@@ -3,6 +3,7 @@ import ComposableArchitecture
 import ElixxirDAppsSDK
 import ErrorFeature
 import LandingFeature
+import MyIdentityFeature
 import SessionFeature
 import SwiftUI
 
@@ -44,7 +45,9 @@ extension AppEnvironment {
       session: SessionEnvironment(
         getClient: { clientSubject.value },
         bgScheduler: bgScheduler,
-        mainScheduler: mainScheduler
+        mainScheduler: mainScheduler,
+        makeId: UUID.init,
+        myIdentity: MyIdentityEnvironment()
       )
     )
   }
diff --git a/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift b/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
index a9b97361..fb4c7200 100644
--- a/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
+++ b/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
@@ -1,7 +1,13 @@
 import ComposableArchitecture
 
 public struct MyIdentityState: Equatable {
-  public init() {}
+  public init(
+    id: UUID
+  ) {
+    self.id = id
+  }
+
+  public var id: UUID
 }
 
 public enum MyIdentityAction: Equatable {}
diff --git a/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift b/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift
index 4061024b..c50941b6 100644
--- a/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift
+++ b/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift
@@ -24,7 +24,7 @@ public struct MyIdentityView_Previews: PreviewProvider {
   public static var previews: some View {
     NavigationView {
       MyIdentityView(store: .init(
-        initialState: .init(),
+        initialState: .init(id: UUID()),
         reducer: .empty,
         environment: ()
       ))
diff --git a/Example/example-app/Sources/SessionFeature/SessionFeature.swift b/Example/example-app/Sources/SessionFeature/SessionFeature.swift
index 0756f738..f3ba9326 100644
--- a/Example/example-app/Sources/SessionFeature/SessionFeature.swift
+++ b/Example/example-app/Sources/SessionFeature/SessionFeature.swift
@@ -2,24 +2,28 @@ import Combine
 import ComposableArchitecture
 import ElixxirDAppsSDK
 import ErrorFeature
+import MyIdentityFeature
 
 public struct SessionState: Equatable {
   public init(
     id: UUID,
     networkFollowerStatus: NetworkFollowerStatus? = nil,
     isNetworkHealthy: Bool? = nil,
-    error: ErrorState? = nil
+    error: ErrorState? = nil,
+    myIdentity: MyIdentityState? = nil
   ) {
     self.id = id
     self.networkFollowerStatus = networkFollowerStatus
     self.isNetworkHealthy = isNetworkHealthy
     self.error = error
+    self.myIdentity = myIdentity
   }
 
   public var id: UUID
   public var networkFollowerStatus: NetworkFollowerStatus?
   public var isNetworkHealthy: Bool?
   public var error: ErrorState?
+  public var myIdentity: MyIdentityState?
 }
 
 public enum SessionAction: Equatable {
@@ -31,23 +35,32 @@ public enum SessionAction: Equatable {
   case monitorNetworkHealth(Bool)
   case didUpdateNetworkHealth(Bool?)
   case didDismissError
+  case presentMyIdentity
+  case didDismissMyIdentity
   case error(ErrorAction)
+  case myIdentity(MyIdentityAction)
 }
 
 public struct SessionEnvironment {
   public init(
     getClient: @escaping () -> Client?,
     bgScheduler: AnySchedulerOf<DispatchQueue>,
-    mainScheduler: AnySchedulerOf<DispatchQueue>
+    mainScheduler: AnySchedulerOf<DispatchQueue>,
+    makeId: @escaping () -> UUID,
+    myIdentity: MyIdentityEnvironment
   ) {
     self.getClient = getClient
     self.bgScheduler = bgScheduler
     self.mainScheduler = mainScheduler
+    self.makeId = makeId
+    self.myIdentity = myIdentity
   }
 
   public var getClient: () -> Client?
   public var bgScheduler: AnySchedulerOf<DispatchQueue>
   public var mainScheduler: AnySchedulerOf<DispatchQueue>
+  public var makeId: () -> UUID
+  public var myIdentity: MyIdentityEnvironment
 }
 
 public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironment>
@@ -129,17 +142,36 @@ public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironm
     state.error = nil
     return .none
 
-  case .error(_):
+  case .presentMyIdentity:
+    if state.myIdentity == nil {
+      state.myIdentity = MyIdentityState(id: env.makeId())
+    }
+    return .none
+
+  case .didDismissMyIdentity:
+    state.myIdentity = nil
+    return .none
+
+  case .error(_), .myIdentity(_):
     return .none
   }
 }
+.presenting(
+  myIdentityReducer,
+  state: .keyPath(\.myIdentity),
+  id: .keyPath(\.?.id),
+  action: /SessionAction.myIdentity,
+  environment: \.myIdentity
+)
 
 #if DEBUG
 extension SessionEnvironment {
   public static let failing = SessionEnvironment(
     getClient: { .failing },
     bgScheduler: .failing,
-    mainScheduler: .failing
+    mainScheduler: .failing,
+    makeId: { fatalError() },
+    myIdentity: .failing
   )
 }
 #endif
diff --git a/Example/example-app/Sources/SessionFeature/SessionView.swift b/Example/example-app/Sources/SessionFeature/SessionView.swift
index 395cfcb1..9f3c419b 100644
--- a/Example/example-app/Sources/SessionFeature/SessionView.swift
+++ b/Example/example-app/Sources/SessionFeature/SessionView.swift
@@ -2,6 +2,7 @@ import ComposableArchitecture
 import ComposablePresentation
 import ElixxirDAppsSDK
 import ErrorFeature
+import MyIdentityFeature
 import SwiftUI
 
 public struct SessionView: View {
@@ -49,6 +50,18 @@ public struct SessionView: View {
         } header: {
           Text("Network health")
         }
+
+        Section {
+          Button {
+            viewStore.send(.presentMyIdentity)
+          } label: {
+            HStack {
+              Text("My identity")
+              Spacer()
+              Image(systemName: "chevron.forward")
+            }
+          }
+        }
       }
       .navigationTitle("Session")
       .task {
@@ -64,6 +77,18 @@ public struct SessionView: View {
         },
         content: ErrorView.init(store:)
       )
+      .background(
+        NavigationLinkWithStore(
+          store.scope(
+            state: \.myIdentity,
+            action: SessionAction.myIdentity
+          ),
+          onDeactivate: {
+            viewStore.send(.didDismissMyIdentity)
+          },
+          destination: MyIdentityView.init(store:)
+        )
+      )
     }
   }
 }
diff --git a/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift b/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift
index 2ada840d..02039168 100644
--- a/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift
+++ b/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift
@@ -1,6 +1,7 @@
 import ComposableArchitecture
 import ElixxirDAppsSDK
 import ErrorFeature
+import MyIdentityFeature
 import XCTest
 @testable import SessionFeature
 
@@ -155,4 +156,25 @@ final class SessionFeatureTests: XCTestCase {
       $0.error = nil
     }
   }
+
+  func testPresentingMyIdentity() {
+    let newId = UUID()
+
+    var env = SessionEnvironment.failing
+    env.makeId = { newId }
+
+    let store = TestStore(
+      initialState: SessionState(id: UUID()),
+      reducer: sessionReducer,
+      environment: env
+    )
+
+    store.send(.presentMyIdentity) {
+      $0.myIdentity = MyIdentityState(id: newId)
+    }
+
+    store.send(.didDismissMyIdentity) {
+      $0.myIdentity = nil
+    }
+  }
 }
-- 
GitLab


From b16ba0e8333e460a91d849eff024309ee15ce9af Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 8 Jun 2022 12:59:07 +0200
Subject: [PATCH 03/13] Combine errorReducer with sessionReducer

---
 Example/example-app/Sources/AppFeature/App.swift      |  1 +
 .../Sources/SessionFeature/SessionFeature.swift       | 11 +++++++++++
 2 files changed, 12 insertions(+)

diff --git a/Example/example-app/Sources/AppFeature/App.swift b/Example/example-app/Sources/AppFeature/App.swift
index 06477b12..16d9b87a 100644
--- a/Example/example-app/Sources/AppFeature/App.swift
+++ b/Example/example-app/Sources/AppFeature/App.swift
@@ -47,6 +47,7 @@ extension AppEnvironment {
         bgScheduler: bgScheduler,
         mainScheduler: mainScheduler,
         makeId: UUID.init,
+        error: ErrorEnvironment(),
         myIdentity: MyIdentityEnvironment()
       )
     )
diff --git a/Example/example-app/Sources/SessionFeature/SessionFeature.swift b/Example/example-app/Sources/SessionFeature/SessionFeature.swift
index f3ba9326..ae1afd73 100644
--- a/Example/example-app/Sources/SessionFeature/SessionFeature.swift
+++ b/Example/example-app/Sources/SessionFeature/SessionFeature.swift
@@ -47,12 +47,14 @@ public struct SessionEnvironment {
     bgScheduler: AnySchedulerOf<DispatchQueue>,
     mainScheduler: AnySchedulerOf<DispatchQueue>,
     makeId: @escaping () -> UUID,
+    error: ErrorEnvironment,
     myIdentity: MyIdentityEnvironment
   ) {
     self.getClient = getClient
     self.bgScheduler = bgScheduler
     self.mainScheduler = mainScheduler
     self.makeId = makeId
+    self.error = error
     self.myIdentity = myIdentity
   }
 
@@ -60,6 +62,7 @@ public struct SessionEnvironment {
   public var bgScheduler: AnySchedulerOf<DispatchQueue>
   public var mainScheduler: AnySchedulerOf<DispatchQueue>
   public var makeId: () -> UUID
+  public var error: ErrorEnvironment
   public var myIdentity: MyIdentityEnvironment
 }
 
@@ -156,6 +159,13 @@ public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironm
     return .none
   }
 }
+.presenting(
+  errorReducer,
+  state: .keyPath(\.error),
+  id: .keyPath(\.?.error),
+  action: /SessionAction.error,
+  environment: \.error
+)
 .presenting(
   myIdentityReducer,
   state: .keyPath(\.myIdentity),
@@ -171,6 +181,7 @@ extension SessionEnvironment {
     bgScheduler: .failing,
     mainScheduler: .failing,
     makeId: { fatalError() },
+    error: .failing,
     myIdentity: .failing
   )
 }
-- 
GitLab


From deb1f482459758298a506a0ff6c737284c764b99 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 8 Jun 2022 13:18:05 +0200
Subject: [PATCH 04/13] Observe identity in MyIdentityFeature

---
 Example/example-app/Package.swift             |  4 ++
 .../example-app/Sources/AppFeature/App.swift  |  8 ++-
 .../MyIdentityFeature/MyIdentityFeature.swift | 59 +++++++++++++++++--
 .../MyIdentityFeature/MyIdentityView.swift    |  3 +
 .../MyIdentityFeatureTests.swift              | 52 +++++++++++++++-
 5 files changed, 119 insertions(+), 7 deletions(-)

diff --git a/Example/example-app/Package.swift b/Example/example-app/Package.swift
index 0c0ef222..72fd8b73 100644
--- a/Example/example-app/Package.swift
+++ b/Example/example-app/Package.swift
@@ -144,6 +144,10 @@ let package = Package(
           name: "ComposableArchitecture",
           package: "swift-composable-architecture"
         ),
+        .product(
+          name: "ElixxirDAppsSDK",
+          package: "elixxir-dapps-sdk-swift"
+        ),
       ],
       swiftSettings: swiftSettings
     ),
diff --git a/Example/example-app/Sources/AppFeature/App.swift b/Example/example-app/Sources/AppFeature/App.swift
index 16d9b87a..37007c70 100644
--- a/Example/example-app/Sources/AppFeature/App.swift
+++ b/Example/example-app/Sources/AppFeature/App.swift
@@ -23,6 +23,7 @@ struct App: SwiftUI.App {
 extension AppEnvironment {
   static func live() -> AppEnvironment {
     let clientSubject = CurrentValueSubject<Client?, Never>(nil)
+    let identitySubject = CurrentValueSubject<Identity?, Never>(nil)
     let mainScheduler = DispatchQueue.main.eraseToAnyScheduler()
     let bgScheduler = DispatchQueue(
       label: "xx.network.dApps.ExampleApp.bg",
@@ -48,7 +49,12 @@ extension AppEnvironment {
         mainScheduler: mainScheduler,
         makeId: UUID.init,
         error: ErrorEnvironment(),
-        myIdentity: MyIdentityEnvironment()
+        myIdentity: MyIdentityEnvironment(
+          getClient: { clientSubject.value },
+          observeIdentity: { identitySubject.eraseToAnyPublisher() },
+          bgScheduler: bgScheduler,
+          mainScheduler: mainScheduler
+        )
       )
     )
   }
diff --git a/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift b/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
index fb4c7200..4fe79ffd 100644
--- a/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
+++ b/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
@@ -1,4 +1,6 @@
+import Combine
 import ComposableArchitecture
+import ElixxirDAppsSDK
 
 public struct MyIdentityState: Equatable {
   public init(
@@ -8,18 +10,67 @@ public struct MyIdentityState: Equatable {
   }
 
   public var id: UUID
+  public var identity: Identity?
 }
 
-public enum MyIdentityAction: Equatable {}
+public enum MyIdentityAction: Equatable {
+  case viewDidLoad
+  case observeMyIdentity
+  case didUpdateMyIdentity(Identity?)
+}
 
 public struct MyIdentityEnvironment {
-  public init() {}
+  public init(
+    getClient: @escaping () -> Client?,
+    observeIdentity: @escaping () -> AnyPublisher<Identity?, Never>,
+    bgScheduler: AnySchedulerOf<DispatchQueue>,
+    mainScheduler: AnySchedulerOf<DispatchQueue>
+  ) {
+    self.getClient = getClient
+    self.observeIdentity = observeIdentity
+    self.bgScheduler = bgScheduler
+    self.mainScheduler = mainScheduler
+  }
+
+  public var getClient: () -> Client?
+  public var observeIdentity: () -> AnyPublisher<Identity?, Never>
+  public var bgScheduler: AnySchedulerOf<DispatchQueue>
+  public var mainScheduler: AnySchedulerOf<DispatchQueue>
 }
 
-public let myIdentityReducer = Reducer<MyIdentityState, MyIdentityAction, MyIdentityEnvironment>.empty
+public let myIdentityReducer = Reducer<MyIdentityState, MyIdentityAction, MyIdentityEnvironment>
+{ state, action, env in
+  switch action {
+  case .viewDidLoad:
+    return .merge([
+      .init(value: .observeMyIdentity),
+    ])
+
+  case .observeMyIdentity:
+    struct EffectId: Hashable {
+      let id: UUID
+    }
+    return env.observeIdentity()
+      .removeDuplicates()
+      .map(MyIdentityAction.didUpdateMyIdentity)
+      .subscribe(on: env.bgScheduler)
+      .receive(on: env.mainScheduler)
+      .eraseToEffect()
+      .cancellable(id: EffectId(id: state.id), cancelInFlight: true)
+
+  case .didUpdateMyIdentity(let identity):
+    state.identity = identity
+    return .none
+  }
+}
 
 #if DEBUG
 extension MyIdentityEnvironment {
-  public static let failing = MyIdentityEnvironment()
+  public static let failing = MyIdentityEnvironment(
+    getClient: { fatalError() },
+    observeIdentity: { fatalError() },
+    bgScheduler: .failing,
+    mainScheduler: .failing
+  )
 }
 #endif
diff --git a/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift b/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift
index c50941b6..62cea2bb 100644
--- a/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift
+++ b/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift
@@ -15,6 +15,9 @@ public struct MyIdentityView: View {
   public var body: some View {
     WithViewStore(store.scope(state: ViewState.init)) { viewStore in
       Text("MyIdentityView")
+        .task {
+          viewStore.send(.viewDidLoad)
+        }
     }
   }
 }
diff --git a/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift b/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift
index f4a6a410..d3fcdc08 100644
--- a/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift
+++ b/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift
@@ -1,9 +1,57 @@
+import Combine
 import ComposableArchitecture
+import ElixxirDAppsSDK
 import XCTest
 @testable import MyIdentityFeature
 
 final class MyIdentityFeatureTests: XCTestCase {
-  func testExample() {
-    XCTAssert(true)
+  func testViewDidLoad() {
+    let myIdentitySubject = PassthroughSubject<Identity?, Never>()
+    let bgScheduler = DispatchQueue.test
+    let mainScheduler = DispatchQueue.test
+
+    var env = MyIdentityEnvironment.failing
+    env.observeIdentity = { myIdentitySubject.eraseToAnyPublisher() }
+    env.bgScheduler = bgScheduler.eraseToAnyScheduler()
+    env.mainScheduler = mainScheduler.eraseToAnyScheduler()
+
+    let store = TestStore(
+      initialState: MyIdentityState(id: UUID()),
+      reducer: myIdentityReducer,
+      environment: env
+    )
+
+    store.send(.viewDidLoad)
+    store.receive(.observeMyIdentity)
+
+    bgScheduler.advance()
+    let identity = Identity.stub()
+    myIdentitySubject.send(identity)
+    mainScheduler.advance()
+
+    store.receive(.didUpdateMyIdentity(identity)) {
+      $0.identity = identity
+    }
+
+    myIdentitySubject.send(nil)
+    mainScheduler.advance()
+
+    store.receive(.didUpdateMyIdentity(nil)) {
+      $0.identity = nil
+    }
+
+    myIdentitySubject.send(completion: .finished)
+    mainScheduler.advance()
+  }
+}
+
+private extension Identity {
+  static func stub() -> Identity {
+    Identity(
+      id: "\(Int.random(in: 100...999))".data(using: .utf8)!,
+      rsaPrivatePem: "\(Int.random(in: 100...999))".data(using: .utf8)!,
+      salt: "\(Int.random(in: 100...999))".data(using: .utf8)!,
+      dhKeyPrivate: "\(Int.random(in: 100...999))".data(using: .utf8)!
+    )
   }
 }
-- 
GitLab


From 7f21e82a6b452b413b7127eda9b4dca04c1f8e7b Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 8 Jun 2022 13:27:36 +0200
Subject: [PATCH 05/13] Make new identity in MyIdentityFeature

---
 .../example-app/Sources/AppFeature/App.swift  |  1 +
 .../MyIdentityFeature/MyIdentityFeature.swift | 23 +++++++++++++
 .../MyIdentityFeatureTests.swift              | 32 +++++++++++++++++++
 3 files changed, 56 insertions(+)

diff --git a/Example/example-app/Sources/AppFeature/App.swift b/Example/example-app/Sources/AppFeature/App.swift
index 37007c70..7a4d3ca9 100644
--- a/Example/example-app/Sources/AppFeature/App.swift
+++ b/Example/example-app/Sources/AppFeature/App.swift
@@ -52,6 +52,7 @@ extension AppEnvironment {
         myIdentity: MyIdentityEnvironment(
           getClient: { clientSubject.value },
           observeIdentity: { identitySubject.eraseToAnyPublisher() },
+          updateIdentity: { identitySubject.value = $0 },
           bgScheduler: bgScheduler,
           mainScheduler: mainScheduler
         )
diff --git a/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift b/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
index 4fe79ffd..bb58876e 100644
--- a/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
+++ b/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
@@ -17,23 +17,28 @@ public enum MyIdentityAction: Equatable {
   case viewDidLoad
   case observeMyIdentity
   case didUpdateMyIdentity(Identity?)
+  case makeIdentity
+  case didFailMakingIdentity(NSError)
 }
 
 public struct MyIdentityEnvironment {
   public init(
     getClient: @escaping () -> Client?,
     observeIdentity: @escaping () -> AnyPublisher<Identity?, Never>,
+    updateIdentity: @escaping (Identity?) -> Void,
     bgScheduler: AnySchedulerOf<DispatchQueue>,
     mainScheduler: AnySchedulerOf<DispatchQueue>
   ) {
     self.getClient = getClient
     self.observeIdentity = observeIdentity
+    self.updateIdentity = updateIdentity
     self.bgScheduler = bgScheduler
     self.mainScheduler = mainScheduler
   }
 
   public var getClient: () -> Client?
   public var observeIdentity: () -> AnyPublisher<Identity?, Never>
+  public var updateIdentity: (Identity?) -> Void
   public var bgScheduler: AnySchedulerOf<DispatchQueue>
   public var mainScheduler: AnySchedulerOf<DispatchQueue>
 }
@@ -61,6 +66,23 @@ public let myIdentityReducer = Reducer<MyIdentityState, MyIdentityAction, MyIden
   case .didUpdateMyIdentity(let identity):
     state.identity = identity
     return .none
+
+  case .makeIdentity:
+    return Effect.run { subscriber in
+      do {
+        env.updateIdentity(try env.getClient()?.makeIdentity())
+      } catch {
+        subscriber.send(.didFailMakingIdentity(error as NSError))
+      }
+      subscriber.send(completion: .finished)
+      return AnyCancellable {}
+    }
+    .subscribe(on: env.bgScheduler)
+    .receive(on: env.mainScheduler)
+    .eraseToEffect()
+
+  case .didFailMakingIdentity(let error):
+    return .none
   }
 }
 
@@ -69,6 +91,7 @@ extension MyIdentityEnvironment {
   public static let failing = MyIdentityEnvironment(
     getClient: { fatalError() },
     observeIdentity: { fatalError() },
+    updateIdentity: { _ in fatalError() },
     bgScheduler: .failing,
     mainScheduler: .failing
   )
diff --git a/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift b/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift
index d3fcdc08..325c1631 100644
--- a/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift
+++ b/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift
@@ -1,5 +1,6 @@
 import Combine
 import ComposableArchitecture
+import CustomDump
 import ElixxirDAppsSDK
 import XCTest
 @testable import MyIdentityFeature
@@ -43,6 +44,37 @@ final class MyIdentityFeatureTests: XCTestCase {
     myIdentitySubject.send(completion: .finished)
     mainScheduler.advance()
   }
+
+  func testMakeIdentity() {
+    let newIdentity = Identity.stub()
+    var didUpdateIdentity = [Identity?]()
+    let bgScheduler = DispatchQueue.test
+    let mainScheduler = DispatchQueue.test
+
+    var env = MyIdentityEnvironment.failing
+    env.getClient = {
+      var client = Client.failing
+      client.makeIdentity.make = { newIdentity }
+      return client
+    }
+    env.updateIdentity = { didUpdateIdentity.append($0) }
+    env.bgScheduler = bgScheduler.eraseToAnyScheduler()
+    env.mainScheduler = mainScheduler.eraseToAnyScheduler()
+
+    let store = TestStore(
+      initialState: MyIdentityState(id: UUID()),
+      reducer: myIdentityReducer,
+      environment: env
+    )
+
+    store.send(.makeIdentity)
+
+    bgScheduler.advance()
+
+    XCTAssertNoDifference(didUpdateIdentity, [newIdentity])
+
+    mainScheduler.advance()
+  }
 }
 
 private extension Identity {
-- 
GitLab


From 6853eb53e229f05a7e7c3f6438284c72fa4387d8 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 8 Jun 2022 13:33:28 +0200
Subject: [PATCH 06/13] Handle failures when making identity

---
 Example/example-app/Package.swift             |  5 +++
 .../example-app/Sources/AppFeature/App.swift  |  3 +-
 .../MyIdentityFeature/MyIdentityFeature.swift | 29 +++++++++++++--
 .../MyIdentityFeatureTests.swift              | 35 +++++++++++++++++++
 4 files changed, 68 insertions(+), 4 deletions(-)

diff --git a/Example/example-app/Package.swift b/Example/example-app/Package.swift
index 72fd8b73..f42e9866 100644
--- a/Example/example-app/Package.swift
+++ b/Example/example-app/Package.swift
@@ -140,10 +140,15 @@ let package = Package(
     .target(
       name: "MyIdentityFeature",
       dependencies: [
+        .target(name: "ErrorFeature"),
         .product(
           name: "ComposableArchitecture",
           package: "swift-composable-architecture"
         ),
+        .product(
+          name: "ComposablePresentation",
+          package: "swift-composable-presentation"
+        ),
         .product(
           name: "ElixxirDAppsSDK",
           package: "elixxir-dapps-sdk-swift"
diff --git a/Example/example-app/Sources/AppFeature/App.swift b/Example/example-app/Sources/AppFeature/App.swift
index 7a4d3ca9..a8b27266 100644
--- a/Example/example-app/Sources/AppFeature/App.swift
+++ b/Example/example-app/Sources/AppFeature/App.swift
@@ -54,7 +54,8 @@ extension AppEnvironment {
           observeIdentity: { identitySubject.eraseToAnyPublisher() },
           updateIdentity: { identitySubject.value = $0 },
           bgScheduler: bgScheduler,
-          mainScheduler: mainScheduler
+          mainScheduler: mainScheduler,
+          error: ErrorEnvironment()
         )
       )
     )
diff --git a/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift b/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
index bb58876e..0b7bb5b7 100644
--- a/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
+++ b/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
@@ -1,16 +1,21 @@
 import Combine
 import ComposableArchitecture
+import ComposablePresentation
 import ElixxirDAppsSDK
+import ErrorFeature
 
 public struct MyIdentityState: Equatable {
   public init(
-    id: UUID
+    id: UUID,
+    error: ErrorState? = nil
   ) {
     self.id = id
+    self.error = error
   }
 
   public var id: UUID
   public var identity: Identity?
+  public var error: ErrorState?
 }
 
 public enum MyIdentityAction: Equatable {
@@ -19,6 +24,8 @@ public enum MyIdentityAction: Equatable {
   case didUpdateMyIdentity(Identity?)
   case makeIdentity
   case didFailMakingIdentity(NSError)
+  case didDismissError
+  case error(ErrorAction)
 }
 
 public struct MyIdentityEnvironment {
@@ -27,13 +34,15 @@ public struct MyIdentityEnvironment {
     observeIdentity: @escaping () -> AnyPublisher<Identity?, Never>,
     updateIdentity: @escaping (Identity?) -> Void,
     bgScheduler: AnySchedulerOf<DispatchQueue>,
-    mainScheduler: AnySchedulerOf<DispatchQueue>
+    mainScheduler: AnySchedulerOf<DispatchQueue>,
+    error: ErrorEnvironment
   ) {
     self.getClient = getClient
     self.observeIdentity = observeIdentity
     self.updateIdentity = updateIdentity
     self.bgScheduler = bgScheduler
     self.mainScheduler = mainScheduler
+    self.error = error
   }
 
   public var getClient: () -> Client?
@@ -41,6 +50,7 @@ public struct MyIdentityEnvironment {
   public var updateIdentity: (Identity?) -> Void
   public var bgScheduler: AnySchedulerOf<DispatchQueue>
   public var mainScheduler: AnySchedulerOf<DispatchQueue>
+  public var error: ErrorEnvironment
 }
 
 public let myIdentityReducer = Reducer<MyIdentityState, MyIdentityAction, MyIdentityEnvironment>
@@ -81,10 +91,22 @@ public let myIdentityReducer = Reducer<MyIdentityState, MyIdentityAction, MyIden
     .receive(on: env.mainScheduler)
     .eraseToEffect()
 
+  case .didDismissError:
+    state.error = nil
+    return .none
+
   case .didFailMakingIdentity(let error):
+    state.error = ErrorState(error: error)
     return .none
   }
 }
+.presenting(
+  errorReducer,
+  state: .keyPath(\.error),
+  id: .keyPath(\.?.error),
+  action: /MyIdentityAction.error,
+  environment: \.error
+)
 
 #if DEBUG
 extension MyIdentityEnvironment {
@@ -93,7 +115,8 @@ extension MyIdentityEnvironment {
     observeIdentity: { fatalError() },
     updateIdentity: { _ in fatalError() },
     bgScheduler: .failing,
-    mainScheduler: .failing
+    mainScheduler: .failing,
+    error: .failing
   )
 }
 #endif
diff --git a/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift b/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift
index 325c1631..64bb06d2 100644
--- a/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift
+++ b/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift
@@ -2,6 +2,7 @@ import Combine
 import ComposableArchitecture
 import CustomDump
 import ElixxirDAppsSDK
+import ErrorFeature
 import XCTest
 @testable import MyIdentityFeature
 
@@ -75,6 +76,40 @@ final class MyIdentityFeatureTests: XCTestCase {
 
     mainScheduler.advance()
   }
+
+  func testMakeIdentityFailure() {
+    let error = NSError(domain: "test", code: 1234)
+    let bgScheduler = DispatchQueue.test
+    let mainScheduler = DispatchQueue.test
+
+    var env = MyIdentityEnvironment.failing
+    env.getClient = {
+      var client = Client.failing
+      client.makeIdentity.make = { throw error }
+      return client
+    }
+    env.bgScheduler = bgScheduler.eraseToAnyScheduler()
+    env.mainScheduler = mainScheduler.eraseToAnyScheduler()
+
+    let store = TestStore(
+      initialState: MyIdentityState(id: UUID()),
+      reducer: myIdentityReducer,
+      environment: env
+    )
+
+    store.send(.makeIdentity)
+
+    bgScheduler.advance()
+    mainScheduler.advance()
+
+    store.receive(.didFailMakingIdentity(error)) {
+      $0.error = ErrorState(error: error)
+    }
+
+    store.send(.didDismissError) {
+      $0.error = nil
+    }
+  }
 }
 
 private extension Identity {
-- 
GitLab


From 1d4953ff425f8aea76c35541343d1e85dc1f6f47 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 8 Jun 2022 13:53:08 +0200
Subject: [PATCH 07/13] Add MyIdentityState.isMakingIdentity property

---
 .../MyIdentityFeature/MyIdentityFeature.swift | 20 ++++++++++++-------
 .../MyIdentityFeatureTests.swift              | 15 +++++++++++---
 2 files changed, 25 insertions(+), 10 deletions(-)

diff --git a/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift b/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
index 0b7bb5b7..8bc65764 100644
--- a/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
+++ b/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
@@ -7,14 +7,17 @@ import ErrorFeature
 public struct MyIdentityState: Equatable {
   public init(
     id: UUID,
+    isMakingIdentity: Bool = false,
     error: ErrorState? = nil
   ) {
     self.id = id
+    self.isMakingIdentity = isMakingIdentity
     self.error = error
   }
 
   public var id: UUID
   public var identity: Identity?
+  public var isMakingIdentity: Bool
   public var error: ErrorState?
 }
 
@@ -23,7 +26,7 @@ public enum MyIdentityAction: Equatable {
   case observeMyIdentity
   case didUpdateMyIdentity(Identity?)
   case makeIdentity
-  case didFailMakingIdentity(NSError)
+  case didFinishMakingIdentity(NSError?)
   case didDismissError
   case error(ErrorAction)
 }
@@ -78,14 +81,14 @@ public let myIdentityReducer = Reducer<MyIdentityState, MyIdentityAction, MyIden
     return .none
 
   case .makeIdentity:
-    return Effect.run { subscriber in
+    state.isMakingIdentity = true
+    return Effect.future { fulfill in
       do {
         env.updateIdentity(try env.getClient()?.makeIdentity())
+        fulfill(.success(.didFinishMakingIdentity(nil)))
       } catch {
-        subscriber.send(.didFailMakingIdentity(error as NSError))
+        fulfill(.success(.didFinishMakingIdentity(error as NSError)))
       }
-      subscriber.send(completion: .finished)
-      return AnyCancellable {}
     }
     .subscribe(on: env.bgScheduler)
     .receive(on: env.mainScheduler)
@@ -95,8 +98,11 @@ public let myIdentityReducer = Reducer<MyIdentityState, MyIdentityAction, MyIden
     state.error = nil
     return .none
 
-  case .didFailMakingIdentity(let error):
-    state.error = ErrorState(error: error)
+  case .didFinishMakingIdentity(let error):
+    state.isMakingIdentity = false
+    if let error = error {
+      state.error = ErrorState(error: error)
+    }
     return .none
   }
 }
diff --git a/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift b/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift
index 64bb06d2..b426cc0f 100644
--- a/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift
+++ b/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift
@@ -68,13 +68,19 @@ final class MyIdentityFeatureTests: XCTestCase {
       environment: env
     )
 
-    store.send(.makeIdentity)
+    store.send(.makeIdentity) {
+      $0.isMakingIdentity = true
+    }
 
     bgScheduler.advance()
 
     XCTAssertNoDifference(didUpdateIdentity, [newIdentity])
 
     mainScheduler.advance()
+
+    store.receive(.didFinishMakingIdentity(nil)) {
+      $0.isMakingIdentity = false
+    }
   }
 
   func testMakeIdentityFailure() {
@@ -97,12 +103,15 @@ final class MyIdentityFeatureTests: XCTestCase {
       environment: env
     )
 
-    store.send(.makeIdentity)
+    store.send(.makeIdentity) {
+      $0.isMakingIdentity = true
+    }
 
     bgScheduler.advance()
     mainScheduler.advance()
 
-    store.receive(.didFailMakingIdentity(error)) {
+    store.receive(.didFinishMakingIdentity(error)) {
+      $0.isMakingIdentity = false
       $0.error = ErrorState(error: error)
     }
 
-- 
GitLab


From f87c132fd00203811d852ba396044f36f79758de Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 8 Jun 2022 14:01:22 +0200
Subject: [PATCH 08/13] Implement MyIdentityView

---
 .../MyIdentityFeature/MyIdentityView.swift    | 67 +++++++++++++++++--
 1 file changed, 63 insertions(+), 4 deletions(-)

diff --git a/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift b/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift
index 62cea2bb..61e09c13 100644
--- a/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift
+++ b/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift
@@ -1,4 +1,7 @@
 import ComposableArchitecture
+import ComposablePresentation
+import ElixxirDAppsSDK
+import ErrorFeature
 import SwiftUI
 
 public struct MyIdentityView: View {
@@ -9,15 +12,71 @@ public struct MyIdentityView: View {
   let store: Store<MyIdentityState, MyIdentityAction>
 
   struct ViewState: Equatable {
-    init(state: MyIdentityState) {}
+    let identity: Identity?
+    let isMakingIdentity: Bool
+
+    init(state: MyIdentityState) {
+      identity = state.identity
+      isMakingIdentity = state.isMakingIdentity
+    }
+
+    var isLoading: Bool {
+      isMakingIdentity
+    }
   }
 
   public var body: some View {
     WithViewStore(store.scope(state: ViewState.init)) { viewStore in
-      Text("MyIdentityView")
-        .task {
-          viewStore.send(.viewDidLoad)
+      Form {
+        Section {
+          Text(string(for: viewStore.identity))
+            .textSelection(.enabled)
+        }
+
+        Section {
+          Button {
+            viewStore.send(.makeIdentity)
+          } label: {
+            HStack {
+              Text("Make new identity")
+              Spacer()
+              if viewStore.isMakingIdentity {
+                ProgressView()
+              }
+            }
+          }
         }
+        .disabled(viewStore.isLoading)
+      }
+      .navigationTitle("My identity")
+      .navigationBarBackButtonHidden(viewStore.isLoading)
+      .task {
+        viewStore.send(.viewDidLoad)
+      }
+      .sheet(
+        store.scope(
+          state: \.error,
+          action: MyIdentityAction.error
+        ),
+        onDismiss: {
+          viewStore.send(.didDismissError)
+        },
+        content: ErrorView.init(store:)
+      )
+    }
+  }
+
+  func string(for identity: Identity?) -> String {
+    guard let identity = identity else {
+      return "No identity"
+    }
+    let encoder = JSONEncoder()
+    encoder.outputFormatting = .prettyPrinted
+    do {
+      let data = try encoder.encode(identity)
+      return String(data: data, encoding: .utf8) ?? "Decoding error"
+    } catch {
+      return "Decoding error: \(error)"
     }
   }
 }
-- 
GitLab


From a047d57ff9e554af09a5fb6b070fef3a52e128b0 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 8 Jun 2022 14:08:41 +0200
Subject: [PATCH 09/13] Add MyContactFeature library to example app

---
 .../xcschemes/MyContactFeature.xcscheme       | 78 +++++++++++++++++++
 .../xcschemes/example-app.xcscheme            | 24 ++++++
 Example/example-app/Package.swift             | 21 +++++
 .../MyContactFeature/MyContactFeature.swift   | 33 ++++++++
 .../MyContactFeature/MyContactView.swift      | 39 ++++++++++
 .../MyContactFeatureTests.swift               |  8 ++
 6 files changed, 203 insertions(+)
 create mode 100644 Example/example-app/.swiftpm/xcode/xcshareddata/xcschemes/MyContactFeature.xcscheme
 create mode 100644 Example/example-app/Sources/MyContactFeature/MyContactFeature.swift
 create mode 100644 Example/example-app/Sources/MyContactFeature/MyContactView.swift
 create mode 100644 Example/example-app/Tests/MyContactFeatureTests/MyContactFeatureTests.swift

diff --git a/Example/example-app/.swiftpm/xcode/xcshareddata/xcschemes/MyContactFeature.xcscheme b/Example/example-app/.swiftpm/xcode/xcshareddata/xcschemes/MyContactFeature.xcscheme
new file mode 100644
index 00000000..60c61fd3
--- /dev/null
+++ b/Example/example-app/.swiftpm/xcode/xcshareddata/xcschemes/MyContactFeature.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 = "MyContactFeature"
+               BuildableName = "MyContactFeature"
+               BlueprintName = "MyContactFeature"
+               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 = "MyContactFeatureTests"
+               BuildableName = "MyContactFeatureTests"
+               BlueprintName = "MyContactFeatureTests"
+               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 = "MyContactFeature"
+            BuildableName = "MyContactFeature"
+            BlueprintName = "MyContactFeature"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/Example/example-app/.swiftpm/xcode/xcshareddata/xcschemes/example-app.xcscheme b/Example/example-app/.swiftpm/xcode/xcshareddata/xcschemes/example-app.xcscheme
index 1a4d13ff..c6edd5dd 100644
--- a/Example/example-app/.swiftpm/xcode/xcshareddata/xcschemes/example-app.xcscheme
+++ b/Example/example-app/.swiftpm/xcode/xcshareddata/xcschemes/example-app.xcscheme
@@ -48,6 +48,20 @@
                ReferencedContainer = "container:">
             </BuildableReference>
          </BuildActionEntry>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "MyContactFeature"
+               BuildableName = "MyContactFeature"
+               BlueprintName = "MyContactFeature"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </BuildActionEntry>
          <BuildActionEntry
             buildForTesting = "YES"
             buildForRunning = "YES"
@@ -115,6 +129,16 @@
                ReferencedContainer = "container:">
             </BuildableReference>
          </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "MyContactFeatureTests"
+               BuildableName = "MyContactFeatureTests"
+               BlueprintName = "MyContactFeatureTests"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </TestableReference>
          <TestableReference
             skipped = "NO">
             <BuildableReference
diff --git a/Example/example-app/Package.swift b/Example/example-app/Package.swift
index f42e9866..d7a464f9 100644
--- a/Example/example-app/Package.swift
+++ b/Example/example-app/Package.swift
@@ -32,6 +32,10 @@ let package = Package(
       name: "LandingFeature",
       targets: ["LandingFeature"]
     ),
+    .library(
+      name: "MyContactFeature",
+      targets: ["MyContactFeature"]
+    ),
     .library(
       name: "MyIdentityFeature",
       targets: ["MyIdentityFeature"]
@@ -137,6 +141,23 @@ let package = Package(
       ],
       swiftSettings: swiftSettings
     ),
+    .target(
+      name: "MyContactFeature",
+      dependencies: [
+        .product(
+          name: "ComposableArchitecture",
+          package: "swift-composable-architecture"
+        ),
+      ],
+      swiftSettings: swiftSettings
+    ),
+    .testTarget(
+      name: "MyContactFeatureTests",
+      dependencies: [
+        .target(name: "MyContactFeature"),
+      ],
+      swiftSettings: swiftSettings
+    ),
     .target(
       name: "MyIdentityFeature",
       dependencies: [
diff --git a/Example/example-app/Sources/MyContactFeature/MyContactFeature.swift b/Example/example-app/Sources/MyContactFeature/MyContactFeature.swift
new file mode 100644
index 00000000..bd520703
--- /dev/null
+++ b/Example/example-app/Sources/MyContactFeature/MyContactFeature.swift
@@ -0,0 +1,33 @@
+import ComposableArchitecture
+
+public struct MyContactState: Equatable {
+  public init(
+    id: UUID
+  ) {
+    self.id = id
+  }
+
+  public var id: UUID
+}
+
+public enum MyContactAction: Equatable {
+  case viewDidLoad
+}
+
+public struct MyContactEnvironment {
+  public init() {}
+}
+
+public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContactEnvironment>
+{ state, action, env in
+  switch action {
+  case .viewDidLoad:
+    return .none
+  }
+}
+
+#if DEBUG
+extension MyContactEnvironment {
+  public static let failing = MyContactEnvironment()
+}
+#endif
diff --git a/Example/example-app/Sources/MyContactFeature/MyContactView.swift b/Example/example-app/Sources/MyContactFeature/MyContactView.swift
new file mode 100644
index 00000000..746a5317
--- /dev/null
+++ b/Example/example-app/Sources/MyContactFeature/MyContactView.swift
@@ -0,0 +1,39 @@
+import ComposableArchitecture
+import SwiftUI
+
+public struct MyContactView: View {
+  public init(store: Store<MyContactState, MyContactAction>) {
+    self.store = store
+  }
+
+  let store: Store<MyContactState, MyContactAction>
+
+  struct ViewState: Equatable {
+    init(state: MyContactState) {}
+  }
+
+  public var body: some View {
+    WithViewStore(store.scope(state: ViewState.init)) { viewStore in
+      Text("MyContactView")
+        .navigationTitle("My contact")
+        .task {
+          viewStore.send(.viewDidLoad)
+        }
+    }
+  }
+}
+
+#if DEBUG
+public struct MyContactView_Previews: PreviewProvider {
+  public static var previews: some View {
+    NavigationView {
+      MyContactView(store: .init(
+        initialState: .init(id: UUID()),
+        reducer: .empty,
+        environment: ()
+      ))
+    }
+    .navigationViewStyle(.stack)
+  }
+}
+#endif
diff --git a/Example/example-app/Tests/MyContactFeatureTests/MyContactFeatureTests.swift b/Example/example-app/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
new file mode 100644
index 00000000..dd026572
--- /dev/null
+++ b/Example/example-app/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
@@ -0,0 +1,8 @@
+import XCTest
+@testable import MyContactFeature
+
+final class MyContactFeatureTests: XCTestCase {
+  func testExample() {
+    XCTAssert(true)
+  }
+}
-- 
GitLab


From b29d338e0fdf0b1bf2f152d09e9cf5ec9dc45b8f Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 8 Jun 2022 14:15:48 +0200
Subject: [PATCH 10/13] Present MyContactView from SessionView

---
 Example/example-app/Package.swift             |  2 ++
 .../example-app/Sources/AppFeature/App.swift  |  4 ++-
 .../SessionFeature/SessionFeature.swift       | 36 ++++++++++++++++---
 .../Sources/SessionFeature/SessionView.swift  | 23 ++++++++++++
 .../SessionFeatureTests.swift                 | 22 ++++++++++++
 5 files changed, 82 insertions(+), 5 deletions(-)

diff --git a/Example/example-app/Package.swift b/Example/example-app/Package.swift
index d7a464f9..faa8a155 100644
--- a/Example/example-app/Package.swift
+++ b/Example/example-app/Package.swift
@@ -66,6 +66,7 @@ let package = Package(
       dependencies: [
         .target(name: "ErrorFeature"),
         .target(name: "LandingFeature"),
+        .target(name: "MyContactFeature"),
         .target(name: "MyIdentityFeature"),
         .target(name: "SessionFeature"),
         .product(
@@ -188,6 +189,7 @@ let package = Package(
       name: "SessionFeature",
       dependencies: [
         .target(name: "ErrorFeature"),
+        .target(name: "MyContactFeature"),
         .target(name: "MyIdentityFeature"),
         .product(
           name: "ComposableArchitecture",
diff --git a/Example/example-app/Sources/AppFeature/App.swift b/Example/example-app/Sources/AppFeature/App.swift
index a8b27266..b106e28a 100644
--- a/Example/example-app/Sources/AppFeature/App.swift
+++ b/Example/example-app/Sources/AppFeature/App.swift
@@ -3,6 +3,7 @@ import ComposableArchitecture
 import ElixxirDAppsSDK
 import ErrorFeature
 import LandingFeature
+import MyContactFeature
 import MyIdentityFeature
 import SessionFeature
 import SwiftUI
@@ -56,7 +57,8 @@ extension AppEnvironment {
           bgScheduler: bgScheduler,
           mainScheduler: mainScheduler,
           error: ErrorEnvironment()
-        )
+        ),
+        myContact: MyContactEnvironment()
       )
     )
   }
diff --git a/Example/example-app/Sources/SessionFeature/SessionFeature.swift b/Example/example-app/Sources/SessionFeature/SessionFeature.swift
index ae1afd73..5b1005a9 100644
--- a/Example/example-app/Sources/SessionFeature/SessionFeature.swift
+++ b/Example/example-app/Sources/SessionFeature/SessionFeature.swift
@@ -2,6 +2,7 @@ import Combine
 import ComposableArchitecture
 import ElixxirDAppsSDK
 import ErrorFeature
+import MyContactFeature
 import MyIdentityFeature
 
 public struct SessionState: Equatable {
@@ -10,13 +11,15 @@ public struct SessionState: Equatable {
     networkFollowerStatus: NetworkFollowerStatus? = nil,
     isNetworkHealthy: Bool? = nil,
     error: ErrorState? = nil,
-    myIdentity: MyIdentityState? = nil
+    myIdentity: MyIdentityState? = nil,
+    myContact: MyContactState? = nil
   ) {
     self.id = id
     self.networkFollowerStatus = networkFollowerStatus
     self.isNetworkHealthy = isNetworkHealthy
     self.error = error
     self.myIdentity = myIdentity
+    self.myContact = myContact
   }
 
   public var id: UUID
@@ -24,6 +27,7 @@ public struct SessionState: Equatable {
   public var isNetworkHealthy: Bool?
   public var error: ErrorState?
   public var myIdentity: MyIdentityState?
+  public var myContact: MyContactState?
 }
 
 public enum SessionAction: Equatable {
@@ -37,8 +41,11 @@ public enum SessionAction: Equatable {
   case didDismissError
   case presentMyIdentity
   case didDismissMyIdentity
+  case presentMyContact
+  case didDismissMyContact
   case error(ErrorAction)
   case myIdentity(MyIdentityAction)
+  case myContact(MyContactAction)
 }
 
 public struct SessionEnvironment {
@@ -48,7 +55,8 @@ public struct SessionEnvironment {
     mainScheduler: AnySchedulerOf<DispatchQueue>,
     makeId: @escaping () -> UUID,
     error: ErrorEnvironment,
-    myIdentity: MyIdentityEnvironment
+    myIdentity: MyIdentityEnvironment,
+    myContact: MyContactEnvironment
   ) {
     self.getClient = getClient
     self.bgScheduler = bgScheduler
@@ -56,6 +64,7 @@ public struct SessionEnvironment {
     self.makeId = makeId
     self.error = error
     self.myIdentity = myIdentity
+    self.myContact = myContact
   }
 
   public var getClient: () -> Client?
@@ -64,6 +73,7 @@ public struct SessionEnvironment {
   public var makeId: () -> UUID
   public var error: ErrorEnvironment
   public var myIdentity: MyIdentityEnvironment
+  public var myContact: MyContactEnvironment
 }
 
 public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironment>
@@ -155,7 +165,17 @@ public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironm
     state.myIdentity = nil
     return .none
 
-  case .error(_), .myIdentity(_):
+  case .presentMyContact:
+    if state.myContact == nil {
+      state.myContact = MyContactState(id: env.makeId())
+    }
+    return .none
+
+  case .didDismissMyContact:
+    state.myContact = nil
+    return .none
+
+  case .error(_), .myIdentity(_), .myContact(_):
     return .none
   }
 }
@@ -173,6 +193,13 @@ public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironm
   action: /SessionAction.myIdentity,
   environment: \.myIdentity
 )
+.presenting(
+  myContactReducer,
+  state: .keyPath(\.myContact),
+  id: .keyPath(\.?.id),
+  action: /SessionAction.myContact,
+  environment: \.myContact
+)
 
 #if DEBUG
 extension SessionEnvironment {
@@ -182,7 +209,8 @@ extension SessionEnvironment {
     mainScheduler: .failing,
     makeId: { fatalError() },
     error: .failing,
-    myIdentity: .failing
+    myIdentity: .failing,
+    myContact: .failing
   )
 }
 #endif
diff --git a/Example/example-app/Sources/SessionFeature/SessionView.swift b/Example/example-app/Sources/SessionFeature/SessionView.swift
index 9f3c419b..ea14d91f 100644
--- a/Example/example-app/Sources/SessionFeature/SessionView.swift
+++ b/Example/example-app/Sources/SessionFeature/SessionView.swift
@@ -2,6 +2,7 @@ import ComposableArchitecture
 import ComposablePresentation
 import ElixxirDAppsSDK
 import ErrorFeature
+import MyContactFeature
 import MyIdentityFeature
 import SwiftUI
 
@@ -61,6 +62,16 @@ public struct SessionView: View {
               Image(systemName: "chevron.forward")
             }
           }
+
+          Button {
+            viewStore.send(.presentMyContact)
+          } label: {
+            HStack {
+              Text("My contact")
+              Spacer()
+              Image(systemName: "chevron.forward")
+            }
+          }
         }
       }
       .navigationTitle("Session")
@@ -89,6 +100,18 @@ public struct SessionView: View {
           destination: MyIdentityView.init(store:)
         )
       )
+      .background(
+        NavigationLinkWithStore(
+          store.scope(
+            state: \.myContact,
+            action: SessionAction.myContact
+          ),
+          onDeactivate: {
+            viewStore.send(.didDismissMyContact)
+          },
+          destination: MyContactView.init(store:)
+        )
+      )
     }
   }
 }
diff --git a/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift b/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift
index 02039168..5c1aa3ba 100644
--- a/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift
+++ b/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift
@@ -1,6 +1,7 @@
 import ComposableArchitecture
 import ElixxirDAppsSDK
 import ErrorFeature
+import MyContactFeature
 import MyIdentityFeature
 import XCTest
 @testable import SessionFeature
@@ -177,4 +178,25 @@ final class SessionFeatureTests: XCTestCase {
       $0.myIdentity = nil
     }
   }
+
+  func testPresentingMyContact() {
+    let newId = UUID()
+
+    var env = SessionEnvironment.failing
+    env.makeId = { newId }
+
+    let store = TestStore(
+      initialState: SessionState(id: UUID()),
+      reducer: sessionReducer,
+      environment: env
+    )
+
+    store.send(.presentMyContact) {
+      $0.myContact = MyContactState(id: newId)
+    }
+
+    store.send(.didDismissMyContact) {
+      $0.myContact = nil
+    }
+  }
 }
-- 
GitLab


From c1908713a1807a43c7b76a231abeb3bca3b3c0e7 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 8 Jun 2022 14:34:33 +0200
Subject: [PATCH 11/13] Add makeContactFromIdentity to Client

---
 Sources/ElixxirDAppsSDK/Client.swift | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/Sources/ElixxirDAppsSDK/Client.swift b/Sources/ElixxirDAppsSDK/Client.swift
index c47d98c4..621270e4 100644
--- a/Sources/ElixxirDAppsSDK/Client.swift
+++ b/Sources/ElixxirDAppsSDK/Client.swift
@@ -9,6 +9,7 @@ public struct Client {
   public var monitorNetworkHealth: NetworkHealthListener
   public var listenErrors: ClientErrorListener
   public var makeIdentity: IdentityMaker
+  public var makeContactFromIdentity: ContactFromIdentityProvider
   public var connect: ConnectionMaker
   public var getContactFromIdentity: ContactFromIdentityProvider
   public var waitForDelivery: MessageDeliveryWaiter
@@ -25,6 +26,7 @@ extension Client {
       monitorNetworkHealth: .live(bindingsClient: bindingsClient),
       listenErrors: .live(bindingsClient: bindingsClient),
       makeIdentity: .live(bindingsClient: bindingsClient),
+      makeContactFromIdentity: .live(bindingsClient: bindingsClient),
       connect: .live(bindingsClient: bindingsClient),
       getContactFromIdentity: .live(bindingsClient: bindingsClient),
       waitForDelivery: .live(bindingsClient: bindingsClient)
@@ -43,6 +45,7 @@ extension Client {
     monitorNetworkHealth: .failing,
     listenErrors: .failing,
     makeIdentity: .failing,
+    makeContactFromIdentity: .failing,
     connect: .failing,
     getContactFromIdentity: .failing,
     waitForDelivery: .failing
-- 
GitLab


From e42c1a97b82bf42f4bd793890352876d19bf801c Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 8 Jun 2022 14:35:33 +0200
Subject: [PATCH 12/13] Update MyIdentityState.init

---
 .../Sources/MyIdentityFeature/MyIdentityFeature.swift            | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift b/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
index 8bc65764..df4559d5 100644
--- a/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
+++ b/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift
@@ -7,6 +7,7 @@ import ErrorFeature
 public struct MyIdentityState: Equatable {
   public init(
     id: UUID,
+    identity: Identity? = nil,
     isMakingIdentity: Bool = false,
     error: ErrorState? = nil
   ) {
-- 
GitLab


From b782cff26d1966a4b35114defc549b5e8619b639 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Wed, 8 Jun 2022 14:52:37 +0200
Subject: [PATCH 13/13] Implement MyContactFeature

---
 Example/example-app/Package.swift             |   9 +
 .../example-app/Sources/AppFeature/App.swift  |  11 +-
 .../MyContactFeature/MyContactFeature.swift   | 111 +++++++++++-
 .../MyContactFeature/MyContactView.swift      |  61 ++++++-
 .../MyContactFeatureTests.swift               | 170 +++++++++++++++++-
 5 files changed, 351 insertions(+), 11 deletions(-)

diff --git a/Example/example-app/Package.swift b/Example/example-app/Package.swift
index faa8a155..6aa45316 100644
--- a/Example/example-app/Package.swift
+++ b/Example/example-app/Package.swift
@@ -145,10 +145,19 @@ let package = Package(
     .target(
       name: "MyContactFeature",
       dependencies: [
+        .target(name: "ErrorFeature"),
         .product(
           name: "ComposableArchitecture",
           package: "swift-composable-architecture"
         ),
+        .product(
+          name: "ComposablePresentation",
+          package: "swift-composable-presentation"
+        ),
+        .product(
+          name: "ElixxirDAppsSDK",
+          package: "elixxir-dapps-sdk-swift"
+        ),
       ],
       swiftSettings: swiftSettings
     ),
diff --git a/Example/example-app/Sources/AppFeature/App.swift b/Example/example-app/Sources/AppFeature/App.swift
index b106e28a..65f4728e 100644
--- a/Example/example-app/Sources/AppFeature/App.swift
+++ b/Example/example-app/Sources/AppFeature/App.swift
@@ -25,6 +25,7 @@ extension AppEnvironment {
   static func live() -> AppEnvironment {
     let clientSubject = CurrentValueSubject<Client?, Never>(nil)
     let identitySubject = CurrentValueSubject<Identity?, Never>(nil)
+    let contactSubject = CurrentValueSubject<Data?, Never>(nil)
     let mainScheduler = DispatchQueue.main.eraseToAnyScheduler()
     let bgScheduler = DispatchQueue(
       label: "xx.network.dApps.ExampleApp.bg",
@@ -58,7 +59,15 @@ extension AppEnvironment {
           mainScheduler: mainScheduler,
           error: ErrorEnvironment()
         ),
-        myContact: MyContactEnvironment()
+        myContact: MyContactEnvironment(
+          getClient: { clientSubject.value },
+          getIdentity: { identitySubject.value },
+          observeContact: { contactSubject.eraseToAnyPublisher() },
+          updateContact: { contactSubject.value = $0 },
+          bgScheduler: bgScheduler,
+          mainScheduler: mainScheduler,
+          error: ErrorEnvironment()
+        )
       )
     )
   }
diff --git a/Example/example-app/Sources/MyContactFeature/MyContactFeature.swift b/Example/example-app/Sources/MyContactFeature/MyContactFeature.swift
index bd520703..317b646f 100644
--- a/Example/example-app/Sources/MyContactFeature/MyContactFeature.swift
+++ b/Example/example-app/Sources/MyContactFeature/MyContactFeature.swift
@@ -1,33 +1,138 @@
+import Combine
 import ComposableArchitecture
+import ComposablePresentation
+import ElixxirDAppsSDK
+import ErrorFeature
 
 public struct MyContactState: Equatable {
   public init(
-    id: UUID
+    id: UUID,
+    contact: Data? = nil,
+    isMakingContact: Bool = false,
+    error: ErrorState? = nil
   ) {
     self.id = id
+    self.contact = contact
+    self.isMakingContact = isMakingContact
+    self.error = error
   }
 
   public var id: UUID
+  public var contact: Data?
+  public var isMakingContact: Bool
+  public var error: ErrorState?
 }
 
 public enum MyContactAction: Equatable {
   case viewDidLoad
+  case observeMyContact
+  case didUpdateMyContact(Data?)
+  case makeContact
+  case didFinishMakingContact(NSError?)
+  case didDismissError
+  case error(ErrorAction)
 }
 
 public struct MyContactEnvironment {
-  public init() {}
+  public init(
+    getClient: @escaping () -> Client?,
+    getIdentity: @escaping () -> Identity?,
+    observeContact: @escaping () -> AnyPublisher<Data?, Never>,
+    updateContact: @escaping (Data?) -> Void,
+    bgScheduler: AnySchedulerOf<DispatchQueue>,
+    mainScheduler: AnySchedulerOf<DispatchQueue>,
+    error: ErrorEnvironment
+  ) {
+    self.getClient = getClient
+    self.getIdentity = getIdentity
+    self.observeContact = observeContact
+    self.updateContact = updateContact
+    self.bgScheduler = bgScheduler
+    self.mainScheduler = mainScheduler
+    self.error = error
+  }
+
+  public var getClient: () -> Client?
+  public var getIdentity: () -> Identity?
+  public var observeContact: () -> AnyPublisher<Data?, Never>
+  public var updateContact: (Data?) -> Void
+  public var bgScheduler: AnySchedulerOf<DispatchQueue>
+  public var mainScheduler: AnySchedulerOf<DispatchQueue>
+  public var error: ErrorEnvironment
 }
 
 public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContactEnvironment>
 { state, action, env in
   switch action {
   case .viewDidLoad:
+    return .merge([
+      .init(value: .observeMyContact),
+    ])
+
+  case .observeMyContact:
+    struct EffectId: Hashable {
+      let id: UUID
+    }
+    return env.observeContact()
+      .removeDuplicates()
+      .map(MyContactAction.didUpdateMyContact)
+      .subscribe(on: env.bgScheduler)
+      .receive(on: env.mainScheduler)
+      .eraseToEffect()
+      .cancellable(id: EffectId(id: state.id), cancelInFlight: true)
+
+  case .didUpdateMyContact(let contact):
+    state.contact = contact
+    return .none
+
+  case .makeContact:
+    state.isMakingContact = true
+    return Effect.future { fulfill in
+      guard let identity = env.getIdentity() else {
+        fulfill(.success(.didFinishMakingContact(NoIdentityError() as NSError)))
+        return
+      }
+      do {
+        env.updateContact(try env.getClient()?.makeContactFromIdentity(identity: identity))
+        fulfill(.success(.didFinishMakingContact(nil)))
+      } catch {
+        fulfill(.success(.didFinishMakingContact(error as NSError)))
+      }
+    }
+    .subscribe(on: env.bgScheduler)
+    .receive(on: env.mainScheduler)
+    .eraseToEffect()
+
+  case .didFinishMakingContact(let error):
+    state.isMakingContact = false
+    if let error = error {
+      state.error = ErrorState(error: error)
+    }
+    return .none
+
+  case .didDismissError:
+    state.error = nil
+    return .none
+
+  case .error(_):
     return .none
   }
 }
 
+public struct NoIdentityError: Error, LocalizedError {
+  public init() {}
+}
+
 #if DEBUG
 extension MyContactEnvironment {
-  public static let failing = MyContactEnvironment()
+  public static let failing = MyContactEnvironment(
+    getClient: { fatalError() },
+    getIdentity: { fatalError() },
+    observeContact: { fatalError() },
+    updateContact: { _ in fatalError() },
+    bgScheduler: .failing,
+    mainScheduler: .failing,
+    error: .failing
+  )
 }
 #endif
diff --git a/Example/example-app/Sources/MyContactFeature/MyContactView.swift b/Example/example-app/Sources/MyContactFeature/MyContactView.swift
index 746a5317..88f9d4b8 100644
--- a/Example/example-app/Sources/MyContactFeature/MyContactView.swift
+++ b/Example/example-app/Sources/MyContactFeature/MyContactView.swift
@@ -1,4 +1,7 @@
 import ComposableArchitecture
+import ComposablePresentation
+import ElixxirDAppsSDK
+import ErrorFeature
 import SwiftUI
 
 public struct MyContactView: View {
@@ -9,17 +12,65 @@ public struct MyContactView: View {
   let store: Store<MyContactState, MyContactAction>
 
   struct ViewState: Equatable {
-    init(state: MyContactState) {}
+    let contact: Data?
+    let isMakingContact: Bool
+
+    init(state: MyContactState) {
+      contact = state.contact
+      isMakingContact = state.isMakingContact
+    }
+
+    var isLoading: Bool {
+      isMakingContact
+    }
   }
 
   public var body: some View {
     WithViewStore(store.scope(state: ViewState.init)) { viewStore in
-      Text("MyContactView")
-        .navigationTitle("My contact")
-        .task {
-          viewStore.send(.viewDidLoad)
+      Form {
+        Section {
+          Text(string(for: viewStore.contact))
+            .textSelection(.enabled)
         }
+
+        Section {
+          Button {
+            viewStore.send(.makeContact)
+          } label: {
+            HStack {
+              Text("Make contact from identity")
+              Spacer()
+              if viewStore.isMakingContact {
+                ProgressView()
+              }
+            }
+          }
+        }
+        .disabled(viewStore.isLoading)
+      }
+      .navigationTitle("My contact")
+      .navigationBarBackButtonHidden(viewStore.isLoading)
+      .task {
+        viewStore.send(.viewDidLoad)
+      }
+      .sheet(
+        store.scope(
+          state: \.error,
+          action: MyContactAction.error
+        ),
+        onDismiss: {
+          viewStore.send(.didDismissError)
+        },
+        content: ErrorView.init(store:)
+      )
+    }
+  }
+
+  func string(for contact: Data?) -> String {
+    guard let contact = contact else {
+      return "No contact"
     }
+    return String(data: contact, encoding: .utf8) ?? "Decoding error"
   }
 }
 
diff --git a/Example/example-app/Tests/MyContactFeatureTests/MyContactFeatureTests.swift b/Example/example-app/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
index dd026572..862be08d 100644
--- a/Example/example-app/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
+++ b/Example/example-app/Tests/MyContactFeatureTests/MyContactFeatureTests.swift
@@ -1,8 +1,174 @@
+import Combine
+import ComposableArchitecture
+import CustomDump
+import ElixxirDAppsSDK
+import ErrorFeature
 import XCTest
 @testable import MyContactFeature
 
 final class MyContactFeatureTests: XCTestCase {
-  func testExample() {
-    XCTAssert(true)
+  func testViewDidLoad() {
+    let myContactSubject = PassthroughSubject<Data?, Never>()
+    let bgScheduler = DispatchQueue.test
+    let mainScheduler = DispatchQueue.test
+
+    var env = MyContactEnvironment.failing
+    env.observeContact = { myContactSubject.eraseToAnyPublisher() }
+    env.bgScheduler = bgScheduler.eraseToAnyScheduler()
+    env.mainScheduler = mainScheduler.eraseToAnyScheduler()
+
+    let store = TestStore(
+      initialState: MyContactState(id: UUID()),
+      reducer: myContactReducer,
+      environment: env
+    )
+
+    store.send(.viewDidLoad)
+    store.receive(.observeMyContact)
+
+    bgScheduler.advance()
+    let contact = "\(Int.random(in: 100...999))".data(using: .utf8)!
+    myContactSubject.send(contact)
+    mainScheduler.advance()
+
+    store.receive(.didUpdateMyContact(contact)) {
+      $0.contact = contact
+    }
+
+    myContactSubject.send(nil)
+    mainScheduler.advance()
+
+    store.receive(.didUpdateMyContact(nil)) {
+      $0.contact = nil
+    }
+
+    myContactSubject.send(completion: .finished)
+    mainScheduler.advance()
+  }
+
+  func testMakeContact() {
+    let identity = Identity.stub()
+    let newContact = "\(Int.random(in: 100...999))".data(using: .utf8)!
+    var didMakeContactFromIdentity = [Identity]()
+    var didUpdateContact = [Data?]()
+    let bgScheduler = DispatchQueue.test
+    let mainScheduler = DispatchQueue.test
+
+    var env = MyContactEnvironment.failing
+    env.getClient = {
+      var client = Client.failing
+      client.makeContactFromIdentity.get = { identity in
+        didMakeContactFromIdentity.append(identity)
+        return newContact
+      }
+      return client
+    }
+    env.updateContact = { didUpdateContact.append($0) }
+    env.getIdentity = { identity }
+    env.bgScheduler = bgScheduler.eraseToAnyScheduler()
+    env.mainScheduler = mainScheduler.eraseToAnyScheduler()
+
+    let store = TestStore(
+      initialState: MyContactState(id: UUID()),
+      reducer: myContactReducer,
+      environment: env
+    )
+
+    store.send(.makeContact) {
+      $0.isMakingContact = true
+    }
+
+    bgScheduler.advance()
+
+    XCTAssertNoDifference(didMakeContactFromIdentity, [identity])
+    XCTAssertNoDifference(didUpdateContact, [newContact])
+
+    mainScheduler.advance()
+
+    store.receive(.didFinishMakingContact(nil)) {
+      $0.isMakingContact = false
+    }
+  }
+
+  func testMakeContactWithoutIdentity() {
+    let error = NoIdentityError() as NSError
+    let bgScheduler = DispatchQueue.test
+    let mainScheduler = DispatchQueue.test
+
+    var env = MyContactEnvironment.failing
+    env.getIdentity = { nil }
+    env.bgScheduler = bgScheduler.eraseToAnyScheduler()
+    env.mainScheduler = mainScheduler.eraseToAnyScheduler()
+
+    let store = TestStore(
+      initialState: MyContactState(id: UUID()),
+      reducer: myContactReducer,
+      environment: env
+    )
+
+    store.send(.makeContact) {
+      $0.isMakingContact = true
+    }
+
+    bgScheduler.advance()
+    mainScheduler.advance()
+
+    store.receive(.didFinishMakingContact(error)) {
+      $0.isMakingContact = false
+      $0.error = ErrorState(error: error)
+    }
+
+    store.send(.didDismissError) {
+      $0.error = nil
+    }
+  }
+
+  func testMakeContactFailure() {
+    let error = NSError(domain: "test", code: 1234)
+    let bgScheduler = DispatchQueue.test
+    let mainScheduler = DispatchQueue.test
+
+    var env = MyContactEnvironment.failing
+    env.getClient = {
+      var client = Client.failing
+      client.makeContactFromIdentity.get = { _ in throw error }
+      return client
+    }
+    env.getIdentity = { .stub() }
+    env.bgScheduler = bgScheduler.eraseToAnyScheduler()
+    env.mainScheduler = mainScheduler.eraseToAnyScheduler()
+
+    let store = TestStore(
+      initialState: MyContactState(id: UUID()),
+      reducer: myContactReducer,
+      environment: env
+    )
+
+    store.send(.makeContact) {
+      $0.isMakingContact = true
+    }
+
+    bgScheduler.advance()
+    mainScheduler.advance()
+
+    store.receive(.didFinishMakingContact(error)) {
+      $0.isMakingContact = false
+      $0.error = ErrorState(error: error)
+    }
+
+    store.send(.didDismissError) {
+      $0.error = nil
+    }
+  }
+}
+
+private extension Identity {
+  static func stub() -> Identity {
+    Identity(
+      id: "\(Int.random(in: 100...999))".data(using: .utf8)!,
+      rsaPrivatePem: "\(Int.random(in: 100...999))".data(using: .utf8)!,
+      salt: "\(Int.random(in: 100...999))".data(using: .utf8)!,
+      dhKeyPrivate: "\(Int.random(in: 100...999))".data(using: .utf8)!
+    )
   }
 }
-- 
GitLab