Skip to content
Snippets Groups Projects
Commit a65d6542 authored by Ahmed Shehata's avatar Ahmed Shehata
Browse files

Merge branch 'feature/example-my-identity' into 'main'

[Example App] Make identity & contact

See merge request elixxir/elixxir-dapps-sdk-swift!14
parents fa1aa40b b782cff2
No related branches found
No related tags found
1 merge request!14[Example App] Make identity & contact
Showing
with 1223 additions and 5 deletions
<?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>
<?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>
......@@ -48,6 +48,34 @@
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"
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 +129,26 @@
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MyContactFeatureTests"
BuildableName = "MyContactFeatureTests"
BlueprintName = "MyContactFeatureTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MyIdentityFeatureTests"
BuildableName = "MyIdentityFeatureTests"
BlueprintName = "MyIdentityFeatureTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
......
......@@ -32,6 +32,14 @@ let package = Package(
name: "LandingFeature",
targets: ["LandingFeature"]
),
.library(
name: "MyContactFeature",
targets: ["MyContactFeature"]
),
.library(
name: "MyIdentityFeature",
targets: ["MyIdentityFeature"]
),
.library(
name: "SessionFeature",
targets: ["SessionFeature"]
......@@ -58,6 +66,8 @@ let package = Package(
dependencies: [
.target(name: "ErrorFeature"),
.target(name: "LandingFeature"),
.target(name: "MyContactFeature"),
.target(name: "MyIdentityFeature"),
.target(name: "SessionFeature"),
.product(
name: "ElixxirDAppsSDK",
......@@ -132,10 +142,64 @@ let package = Package(
],
swiftSettings: swiftSettings
),
.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
),
.testTarget(
name: "MyContactFeatureTests",
dependencies: [
.target(name: "MyContactFeature"),
],
swiftSettings: swiftSettings
),
.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"
),
],
swiftSettings: swiftSettings
),
.testTarget(
name: "MyIdentityFeatureTests",
dependencies: [
.target(name: "MyIdentityFeature"),
],
swiftSettings: swiftSettings
),
.target(
name: "SessionFeature",
dependencies: [
.target(name: "ErrorFeature"),
.target(name: "MyContactFeature"),
.target(name: "MyIdentityFeature"),
.product(
name: "ComposableArchitecture",
package: "swift-composable-architecture"
......
......@@ -3,6 +3,8 @@ import ComposableArchitecture
import ElixxirDAppsSDK
import ErrorFeature
import LandingFeature
import MyContactFeature
import MyIdentityFeature
import SessionFeature
import SwiftUI
......@@ -22,6 +24,8 @@ struct App: SwiftUI.App {
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",
......@@ -44,7 +48,26 @@ extension AppEnvironment {
session: SessionEnvironment(
getClient: { clientSubject.value },
bgScheduler: bgScheduler,
mainScheduler: mainScheduler
mainScheduler: mainScheduler,
makeId: UUID.init,
error: ErrorEnvironment(),
myIdentity: MyIdentityEnvironment(
getClient: { clientSubject.value },
observeIdentity: { identitySubject.eraseToAnyPublisher() },
updateIdentity: { identitySubject.value = $0 },
bgScheduler: bgScheduler,
mainScheduler: mainScheduler,
error: ErrorEnvironment()
),
myContact: MyContactEnvironment(
getClient: { clientSubject.value },
getIdentity: { identitySubject.value },
observeContact: { contactSubject.eraseToAnyPublisher() },
updateContact: { contactSubject.value = $0 },
bgScheduler: bgScheduler,
mainScheduler: mainScheduler,
error: ErrorEnvironment()
)
)
)
}
......
import Combine
import ComposableArchitecture
import ComposablePresentation
import ElixxirDAppsSDK
import ErrorFeature
public struct MyContactState: Equatable {
public init(
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(
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(
getClient: { fatalError() },
getIdentity: { fatalError() },
observeContact: { fatalError() },
updateContact: { _ in fatalError() },
bgScheduler: .failing,
mainScheduler: .failing,
error: .failing
)
}
#endif
import ComposableArchitecture
import ComposablePresentation
import ElixxirDAppsSDK
import ErrorFeature
import SwiftUI
public struct MyContactView: View {
public init(store: Store<MyContactState, MyContactAction>) {
self.store = store
}
let store: Store<MyContactState, MyContactAction>
struct ViewState: Equatable {
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
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"
}
}
#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
import Combine
import ComposableArchitecture
import ComposablePresentation
import ElixxirDAppsSDK
import ErrorFeature
public struct MyIdentityState: Equatable {
public init(
id: UUID,
identity: Identity? = nil,
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?
}
public enum MyIdentityAction: Equatable {
case viewDidLoad
case observeMyIdentity
case didUpdateMyIdentity(Identity?)
case makeIdentity
case didFinishMakingIdentity(NSError?)
case didDismissError
case error(ErrorAction)
}
public struct MyIdentityEnvironment {
public init(
getClient: @escaping () -> Client?,
observeIdentity: @escaping () -> AnyPublisher<Identity?, Never>,
updateIdentity: @escaping (Identity?) -> Void,
bgScheduler: 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?
public var observeIdentity: () -> AnyPublisher<Identity?, Never>
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>
{ 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
case .makeIdentity:
state.isMakingIdentity = true
return Effect.future { fulfill in
do {
env.updateIdentity(try env.getClient()?.makeIdentity())
fulfill(.success(.didFinishMakingIdentity(nil)))
} catch {
fulfill(.success(.didFinishMakingIdentity(error as NSError)))
}
}
.subscribe(on: env.bgScheduler)
.receive(on: env.mainScheduler)
.eraseToEffect()
case .didDismissError:
state.error = nil
return .none
case .didFinishMakingIdentity(let error):
state.isMakingIdentity = false
if let error = 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 {
public static let failing = MyIdentityEnvironment(
getClient: { fatalError() },
observeIdentity: { fatalError() },
updateIdentity: { _ in fatalError() },
bgScheduler: .failing,
mainScheduler: .failing,
error: .failing
)
}
#endif
import ComposableArchitecture
import ComposablePresentation
import ElixxirDAppsSDK
import ErrorFeature
import SwiftUI
public struct MyIdentityView: View {
public init(store: Store<MyIdentityState, MyIdentityAction>) {
self.store = store
}
let store: Store<MyIdentityState, MyIdentityAction>
struct ViewState: Equatable {
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
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)"
}
}
}
#if DEBUG
public struct MyIdentityView_Previews: PreviewProvider {
public static var previews: some View {
NavigationView {
MyIdentityView(store: .init(
initialState: .init(id: UUID()),
reducer: .empty,
environment: ()
))
}
.navigationViewStyle(.stack)
}
}
#endif
......@@ -2,24 +2,32 @@ import Combine
import ComposableArchitecture
import ElixxirDAppsSDK
import ErrorFeature
import MyContactFeature
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,
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
public var networkFollowerStatus: NetworkFollowerStatus?
public var isNetworkHealthy: Bool?
public var error: ErrorState?
public var myIdentity: MyIdentityState?
public var myContact: MyContactState?
}
public enum SessionAction: Equatable {
......@@ -31,23 +39,41 @@ public enum SessionAction: Equatable {
case monitorNetworkHealth(Bool)
case didUpdateNetworkHealth(Bool?)
case didDismissError
case presentMyIdentity
case didDismissMyIdentity
case presentMyContact
case didDismissMyContact
case error(ErrorAction)
case myIdentity(MyIdentityAction)
case myContact(MyContactAction)
}
public struct SessionEnvironment {
public init(
getClient: @escaping () -> Client?,
bgScheduler: AnySchedulerOf<DispatchQueue>,
mainScheduler: AnySchedulerOf<DispatchQueue>
mainScheduler: AnySchedulerOf<DispatchQueue>,
makeId: @escaping () -> UUID,
error: ErrorEnvironment,
myIdentity: MyIdentityEnvironment,
myContact: MyContactEnvironment
) {
self.getClient = getClient
self.bgScheduler = bgScheduler
self.mainScheduler = mainScheduler
self.makeId = makeId
self.error = error
self.myIdentity = myIdentity
self.myContact = myContact
}
public var getClient: () -> Client?
public var bgScheduler: AnySchedulerOf<DispatchQueue>
public var mainScheduler: AnySchedulerOf<DispatchQueue>
public var makeId: () -> UUID
public var error: ErrorEnvironment
public var myIdentity: MyIdentityEnvironment
public var myContact: MyContactEnvironment
}
public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironment>
......@@ -129,17 +155,62 @@ 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 .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
}
}
.presenting(
errorReducer,
state: .keyPath(\.error),
id: .keyPath(\.?.error),
action: /SessionAction.error,
environment: \.error
)
.presenting(
myIdentityReducer,
state: .keyPath(\.myIdentity),
id: .keyPath(\.?.id),
action: /SessionAction.myIdentity,
environment: \.myIdentity
)
.presenting(
myContactReducer,
state: .keyPath(\.myContact),
id: .keyPath(\.?.id),
action: /SessionAction.myContact,
environment: \.myContact
)
#if DEBUG
extension SessionEnvironment {
public static let failing = SessionEnvironment(
getClient: { .failing },
bgScheduler: .failing,
mainScheduler: .failing
mainScheduler: .failing,
makeId: { fatalError() },
error: .failing,
myIdentity: .failing,
myContact: .failing
)
}
#endif
......@@ -2,6 +2,8 @@ import ComposableArchitecture
import ComposablePresentation
import ElixxirDAppsSDK
import ErrorFeature
import MyContactFeature
import MyIdentityFeature
import SwiftUI
public struct SessionView: View {
......@@ -49,6 +51,28 @@ public struct SessionView: View {
} header: {
Text("Network health")
}
Section {
Button {
viewStore.send(.presentMyIdentity)
} label: {
HStack {
Text("My identity")
Spacer()
Image(systemName: "chevron.forward")
}
}
Button {
viewStore.send(.presentMyContact)
} label: {
HStack {
Text("My contact")
Spacer()
Image(systemName: "chevron.forward")
}
}
}
}
.navigationTitle("Session")
.task {
......@@ -64,6 +88,30 @@ 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:)
)
)
.background(
NavigationLinkWithStore(
store.scope(
state: \.myContact,
action: SessionAction.myContact
),
onDeactivate: {
viewStore.send(.didDismissMyContact)
},
destination: MyContactView.init(store:)
)
)
}
}
}
......
import Combine
import ComposableArchitecture
import CustomDump
import ElixxirDAppsSDK
import ErrorFeature
import XCTest
@testable import MyContactFeature
final class MyContactFeatureTests: XCTestCase {
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)!
)
}
}
import Combine
import ComposableArchitecture
import CustomDump
import ElixxirDAppsSDK
import ErrorFeature
import XCTest
@testable import MyIdentityFeature
final class MyIdentityFeatureTests: XCTestCase {
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()
}
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) {
$0.isMakingIdentity = true
}
bgScheduler.advance()
XCTAssertNoDifference(didUpdateIdentity, [newIdentity])
mainScheduler.advance()
store.receive(.didFinishMakingIdentity(nil)) {
$0.isMakingIdentity = false
}
}
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) {
$0.isMakingIdentity = true
}
bgScheduler.advance()
mainScheduler.advance()
store.receive(.didFinishMakingIdentity(error)) {
$0.isMakingIdentity = 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)!
)
}
}
import ComposableArchitecture
import ElixxirDAppsSDK
import ErrorFeature
import MyContactFeature
import MyIdentityFeature
import XCTest
@testable import SessionFeature
......@@ -155,4 +157,46 @@ 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
}
}
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
}
}
}
......@@ -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
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment