diff --git a/Example/example-app/Package.swift b/Example/example-app/Package.swift index 6e03443ca97f353bccbe0f8b40b914a74cdfdeb5..47c0ccbdf4f4b7ec1c094b9bec248c58ec726f8f 100644 --- a/Example/example-app/Package.swift +++ b/Example/example-app/Package.swift @@ -32,14 +32,6 @@ let package = Package( name: "LandingFeature", targets: ["LandingFeature"] ), - .library( - name: "MyContactFeature", - targets: ["MyContactFeature"] - ), - .library( - name: "MyIdentityFeature", - targets: ["MyIdentityFeature"] - ), .library( name: "SessionFeature", targets: ["SessionFeature"] @@ -156,58 +148,6 @@ 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: [ diff --git a/Example/example-app/Sources/MyContactFeature/MyContactFeature.swift b/Example/example-app/Sources/MyContactFeature/MyContactFeature.swift deleted file mode 100644 index 317b646f1f7b5897f1cd4b4829f41cd8e67fd259..0000000000000000000000000000000000000000 --- a/Example/example-app/Sources/MyContactFeature/MyContactFeature.swift +++ /dev/null @@ -1,138 +0,0 @@ -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 diff --git a/Example/example-app/Sources/MyContactFeature/MyContactView.swift b/Example/example-app/Sources/MyContactFeature/MyContactView.swift deleted file mode 100644 index 88f9d4b8fa84272d7ef3695fed506410d4010aa1..0000000000000000000000000000000000000000 --- a/Example/example-app/Sources/MyContactFeature/MyContactView.swift +++ /dev/null @@ -1,90 +0,0 @@ -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 diff --git a/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift b/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift deleted file mode 100644 index df4559d5cb56cb3f47c0ad79c57bbe5c0296a3bb..0000000000000000000000000000000000000000 --- a/Example/example-app/Sources/MyIdentityFeature/MyIdentityFeature.swift +++ /dev/null @@ -1,129 +0,0 @@ -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 diff --git a/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift b/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift deleted file mode 100644 index 61e09c1355755a7f1955e91ad7054d78f3689f5a..0000000000000000000000000000000000000000 --- a/Example/example-app/Sources/MyIdentityFeature/MyIdentityView.swift +++ /dev/null @@ -1,97 +0,0 @@ -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 diff --git a/Example/example-app/Tests/MyContactFeatureTests/MyContactFeatureTests.swift b/Example/example-app/Tests/MyContactFeatureTests/MyContactFeatureTests.swift deleted file mode 100644 index 862be08da221c884a14875b3ceb86010788073f4..0000000000000000000000000000000000000000 --- a/Example/example-app/Tests/MyContactFeatureTests/MyContactFeatureTests.swift +++ /dev/null @@ -1,174 +0,0 @@ -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)! - ) - } -} diff --git a/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift b/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift deleted file mode 100644 index b426cc0fbd26c49bc14b462db938b1eb27afb712..0000000000000000000000000000000000000000 --- a/Example/example-app/Tests/MyIdentityFeatureTests/MyIdentityFeatureTests.swift +++ /dev/null @@ -1,133 +0,0 @@ -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)! - ) - } -}