diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 4092af826336f082c259e6f062ba40d86380852b..c45c93ef4fb5b3f3f7052d1b99aa15c1b038709b 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -16,6 +16,12 @@ extension AppEnvironment { let dbManager = DBManager.live() let messengerEnv = MessengerEnvironment.live() let messenger = Messenger.live(messengerEnv) + let authHandler = AuthCallbackHandler.live( + messenger: messenger, + handleRequest: .live(db: dbManager.getDB, now: Date.init), + handleConfirm: .live(db: dbManager.getDB), + handleReset: .live(db: dbManager.getDB) + ) let mainQueue = DispatchQueue.main.eraseToAnyScheduler() let bgQueue = DispatchQueue.global(qos: .background).eraseToAnyScheduler() @@ -53,9 +59,9 @@ extension AppEnvironment { HomeEnvironment( messenger: messenger, dbManager: dbManager, + authHandler: authHandler, mainQueue: mainQueue, bgQueue: bgQueue, - now: Date.init, register: { RegisterEnvironment( messenger: messenger, diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift index 985ef3293ca3abdab70532f0d40f99676a1b29fb..e28008f39e924af1c5816bb33c9e4dce11798476 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift @@ -14,6 +14,7 @@ import XXModels public struct HomeState: Equatable { public init( failure: String? = nil, + authFailure: String? = nil, isNetworkHealthy: Bool? = nil, networkNodesReport: NodeRegistrationReport? = nil, isDeletingAccount: Bool = false, @@ -23,6 +24,7 @@ public struct HomeState: Equatable { userSearch: UserSearchState? = nil ) { self.failure = failure + self.authFailure = authFailure self.isNetworkHealthy = isNetworkHealthy self.isDeletingAccount = isDeletingAccount self.alert = alert @@ -32,6 +34,7 @@ public struct HomeState: Equatable { } public var failure: String? + public var authFailure: String? public var isNetworkHealthy: Bool? public var networkNodesReport: NodeRegistrationReport? public var isDeletingAccount: Bool @@ -49,10 +52,11 @@ public enum HomeAction: Equatable { case failure(NSError) } - public enum AuthCallbacks: Equatable { - case register - case unregister - case handle(XXClient.AuthCallbacks.Callback) + public enum AuthHandler: Equatable { + case start + case stop + case failure(NSError) + case failureDismissed } public enum NetworkMonitor: Equatable { @@ -70,7 +74,7 @@ public enum HomeAction: Equatable { } case messenger(Messenger) - case authCallbacks(AuthCallbacks) + case authHandler(AuthHandler) case networkMonitor(NetworkMonitor) case deleteAccount(DeleteAccount) case didDismissAlert @@ -88,18 +92,18 @@ public struct HomeEnvironment { public init( messenger: Messenger, dbManager: DBManager, + authHandler: AuthCallbackHandler, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue>, - now: @escaping () -> Date, register: @escaping () -> RegisterEnvironment, contacts: @escaping () -> ContactsEnvironment, userSearch: @escaping () -> UserSearchEnvironment ) { self.messenger = messenger self.dbManager = dbManager + self.authHandler = authHandler self.mainQueue = mainQueue self.bgQueue = bgQueue - self.now = now self.register = register self.contacts = contacts self.userSearch = userSearch @@ -107,9 +111,9 @@ public struct HomeEnvironment { public var messenger: Messenger public var dbManager: DBManager + public var authHandler: AuthCallbackHandler public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> - public var now: () -> Date public var register: () -> RegisterEnvironment public var contacts: () -> ContactsEnvironment public var userSearch: () -> UserSearchEnvironment @@ -119,9 +123,9 @@ extension HomeEnvironment { public static let unimplemented = HomeEnvironment( messenger: .unimplemented, dbManager: .unimplemented, + authHandler: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented, - now: XCTUnimplemented("\(Self.self).now", placeholder: Date()), register: { .unimplemented }, contacts: { .unimplemented }, userSearch: { .unimplemented } @@ -137,7 +141,7 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> switch action { case .messenger(.start): return .merge( - Effect(value: .authCallbacks(.register)), + Effect(value: .authHandler(.start)), Effect(value: .networkMonitor(.stop)), Effect.result { do { @@ -175,12 +179,11 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> state.failure = error.localizedDescription return .none - case .authCallbacks(.register): + case .authHandler(.start): return Effect.run { subscriber in - let handler = AuthCallbacks { callback in - subscriber.send(.authCallbacks(.handle(callback))) - } - let cancellable = env.messenger.registerAuthCallbacks(handler) + let cancellable = env.authHandler(onError: { error in + subscriber.send(.authHandler(.failure(error as NSError))) + }) return AnyCancellable { cancellable.cancel() } } .subscribe(on: env.bgQueue) @@ -188,44 +191,15 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> .eraseToEffect() .cancellable(id: AuthCallbacksEffectId.self, cancelInFlight: true) - case .authCallbacks(.unregister): + case .authHandler(.stop): return .cancel(id: AuthCallbacksEffectId.self) - case .authCallbacks(.handle(.request(let contact, _, _, _))): - return .fireAndForget { - let db = try env.dbManager.getDB() - let contactId = try contact.getId() - guard try db.fetchContacts(.init(id: [contactId])).isEmpty else { - return - } - var dbContact = XXModels.Contact(id: contactId) - dbContact.marshaled = contact.data - dbContact.username = try contact.getFact(.username)?.value - dbContact.email = try contact.getFact(.email)?.value - dbContact.phone = try contact.getFact(.phone)?.value - dbContact.authStatus = .verificationInProgress - dbContact.createdAt = env.now() - dbContact = try db.saveContact(dbContact) - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .authCallbacks(.handle(.confirm(let contact, _, _, _))): - return .fireAndForget { - let db = try env.dbManager.getDB() - let contactId = try contact.getId() - guard var dbContact = try db.fetchContacts(.init(id: [contactId])).first else { - return - } - dbContact.authStatus = .friend - dbContact = try db.saveContact(dbContact) - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() + case .authHandler(.failure(let error)): + state.authFailure = error.localizedDescription + return .none - case .authCallbacks(.handle(.reset(let contact, _, _, _))): + case .authHandler(.failureDismissed): + state.authFailure = nil return .none case .networkMonitor(.start): diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift index f1abfb2b9ebf3d4b2ad4d20aa477b2f49d96fb80..f6a964896e6e67544e693d83cb26378756414b86 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift @@ -15,12 +15,14 @@ public struct HomeView: View { struct ViewState: Equatable { var failure: String? + var authFailure: String? var isNetworkHealthy: Bool? var networkNodesReport: NodeRegistrationReport? var isDeletingAccount: Bool init(state: HomeState) { failure = state.failure + authFailure = state.authFailure isNetworkHealthy = state.isNetworkHealthy isDeletingAccount = state.isDeletingAccount networkNodesReport = state.networkNodesReport @@ -31,21 +33,32 @@ public struct HomeView: View { WithViewStore(store, observe: ViewState.init) { viewStore in NavigationView { Form { - Section { - if let failure = viewStore.failure { + if let failure = viewStore.failure { + Section { Text(failure) Button { viewStore.send(.messenger(.start)) } label: { Text("Retry") } - } - } header: { - if viewStore.failure != nil { + } header: { Text("Error") } } + if let authFailure = viewStore.authFailure { + Section { + Text(authFailure) + Button { + viewStore.send(.authHandler(.failureDismissed)) + } label: { + Text("Dismiss") + } + } header: { + Text("Auth Error") + } + } + Section { HStack { Text("Health") diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift index 40cd5a2a7e2fbf4cd586b667d5c2aa9e36392db2..f3e5bcf8f787e963820b4a33cd09cb31f4494fd5 100644 --- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift @@ -1,3 +1,4 @@ +import AppCore import ComposableArchitecture import ContactsFeature import RegisterFeature @@ -21,7 +22,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate - store.environment.messenger.registerAuthCallbacks.run = { _ in Cancellable {} } + store.environment.authHandler.run = { _ in Cancellable {} } store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } store.environment.messenger.isConnected.run = { false } store.environment.messenger.connect.run = { messengerDidConnect += 1 } @@ -33,13 +34,13 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000]) XCTAssertNoDifference(messengerDidConnect, 1) - store.receive(.authCallbacks(.register)) + store.receive(.authHandler(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartUnregistered)) { $0.register = RegisterState() } - store.send(.authCallbacks(.unregister)) + store.send(.authHandler(.stop)) } func testMessengerStartRegistered() { @@ -55,7 +56,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate - store.environment.messenger.registerAuthCallbacks.run = { _ in Cancellable {} } + store.environment.authHandler.run = { _ in Cancellable {} } store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } store.environment.messenger.isConnected.run = { false } store.environment.messenger.connect.run = { messengerDidConnect += 1 } @@ -78,14 +79,13 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidConnect, 1) XCTAssertNoDifference(messengerDidLogIn, 1) - store.receive(.authCallbacks(.register)) + store.receive(.authHandler(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartRegistered)) store.receive(.networkMonitor(.start)) store.send(.networkMonitor(.stop)) - - store.send(.authCallbacks(.unregister)) + store.send(.authHandler(.stop)) } func testRegisterFinished() { @@ -102,7 +102,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate - store.environment.messenger.registerAuthCallbacks.run = { _ in Cancellable {} } + store.environment.authHandler.run = { _ in Cancellable {} } store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } store.environment.messenger.isConnected.run = { true } store.environment.messenger.isLoggedIn.run = { false } @@ -127,14 +127,13 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000]) XCTAssertNoDifference(messengerDidLogIn, 1) - store.receive(.authCallbacks(.register)) + store.receive(.authHandler(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartRegistered)) store.receive(.networkMonitor(.start)) store.send(.networkMonitor(.stop)) - - store.send(.authCallbacks(.unregister)) + store.send(.authHandler(.stop)) } func testMessengerStartFailure() { @@ -149,18 +148,18 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate - store.environment.messenger.registerAuthCallbacks.run = { _ in Cancellable {} } + store.environment.authHandler.run = { _ in Cancellable {} } store.environment.messenger.start.run = { _ in throw error } store.send(.messenger(.start)) - store.receive(.authCallbacks(.register)) + store.receive(.authHandler(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } - store.send(.authCallbacks(.unregister)) + store.send(.authHandler(.stop)) } func testMessengerStartConnectFailure() { @@ -175,20 +174,20 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate - store.environment.messenger.registerAuthCallbacks.run = { _ in Cancellable {} } + store.environment.authHandler.run = { _ in Cancellable {} } store.environment.messenger.start.run = { _ in } store.environment.messenger.isConnected.run = { false } store.environment.messenger.connect.run = { throw error } store.send(.messenger(.start)) - store.receive(.authCallbacks(.register)) + store.receive(.authHandler(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } - store.send(.authCallbacks(.unregister)) + store.send(.authHandler(.stop)) } func testMessengerStartIsRegisteredFailure() { @@ -203,7 +202,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate - store.environment.messenger.registerAuthCallbacks.run = { _ in Cancellable {} } + store.environment.authHandler.run = { _ in Cancellable {} } store.environment.messenger.start.run = { _ in } store.environment.messenger.isConnected.run = { true } store.environment.messenger.isLoggedIn.run = { false } @@ -211,13 +210,13 @@ final class HomeFeatureTests: XCTestCase { store.send(.messenger(.start)) - store.receive(.authCallbacks(.register)) + store.receive(.authHandler(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } - store.send(.authCallbacks(.unregister)) + store.send(.authHandler(.stop)) } func testMessengerStartLogInFailure() { @@ -232,7 +231,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate - store.environment.messenger.registerAuthCallbacks.run = { _ in Cancellable {} } + store.environment.authHandler.run = { _ in Cancellable {} } store.environment.messenger.start.run = { _ in } store.environment.messenger.isConnected.run = { true } store.environment.messenger.isLoggedIn.run = { false } @@ -241,13 +240,13 @@ final class HomeFeatureTests: XCTestCase { store.send(.messenger(.start)) - store.receive(.authCallbacks(.register)) + store.receive(.authHandler(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } - store.send(.authCallbacks(.unregister)) + store.send(.authHandler(.stop)) } func testNetworkMonitorStart() { @@ -527,101 +526,37 @@ final class HomeFeatureTests: XCTestCase { environment: .unimplemented ) - let now = Date() - var didRegisterCallbacks: [AuthCallbacks] = [] - var didCancelAuthCallbacks = 0 - var dbContact: XXModels.Contact? - var dbDidSaveContact: [XXModels.Contact] = [] + var didRunAuthHandler = 0 + var didCancelAuthHandler = 0 + var authHandlerOnError: [AuthCallbackHandler.OnError] = [] store.environment.mainQueue = .immediate store.environment.bgQueue = .immediate - store.environment.now = { now } - store.environment.messenger.registerAuthCallbacks.run = { callbacks in - didRegisterCallbacks.append(callbacks) - return Cancellable { didCancelAuthCallbacks += 1 } - } - store.environment.dbManager.getDB.run = { - var db: Database = .failing - db.fetchContacts.run = { _ in [dbContact].compactMap { $0 } } - db.saveContact.run = { contact in - dbDidSaveContact.append(contact) - return contact - } - return db - } - - store.send(.authCallbacks(.register)) - - XCTAssertNoDifference(didRegisterCallbacks.count, 1) - - var contact = XXClient.Contact.unimplemented("data".data(using: .utf8)!) - contact.getIdFromContact.run = { _ in "id".data(using: .utf8)! } - contact.getFactsFromContact.run = { _ in - [ - Fact(type: .username, value: "username"), - Fact(type: .email, value: "email"), - Fact(type: .phone, value: "phone"), - ] + store.environment.authHandler.run = { onError in + didRunAuthHandler += 1 + authHandlerOnError.append(onError) + return Cancellable { didCancelAuthHandler += 1 } } - dbContact = nil - dbDidSaveContact = [] - didRegisterCallbacks.first?.handle( - .request(contact: contact, receptionId: Data(), ephemeralId: 0, roundId: 0) - ) + store.send(.authHandler(.start)) - store.receive(.authCallbacks(.handle( - .request(contact: contact, receptionId: Data(), ephemeralId: 0, roundId: 0) - ))) - XCTAssertNoDifference(dbDidSaveContact, [.init( - id: "id".data(using: .utf8)!, - marshaled: "data".data(using: .utf8)!, - username: "username", - email: "email", - phone: "phone", - authStatus: .verificationInProgress, - createdAt: now - )]) - - dbContact = .init(id: "id".data(using: .utf8)!) - dbDidSaveContact = [] - didRegisterCallbacks.first?.handle( - .request(contact: contact, receptionId: Data(), ephemeralId: 0, roundId: 0) - ) + XCTAssertNoDifference(didRunAuthHandler, 1) - store.receive(.authCallbacks(.handle( - .request(contact: contact, receptionId: Data(), ephemeralId: 0, roundId: 0) - ))) - XCTAssertNoDifference(dbDidSaveContact, []) + struct AuthHandlerError: Error { var id: Int } + authHandlerOnError.first?(AuthHandlerError(id: 1)) - dbContact = .init(id: "id".data(using: .utf8)!) - dbDidSaveContact = [] - didRegisterCallbacks.first?.handle( - .confirm(contact: contact, receptionId: Data(), ephemeralId: 0, roundId: 0) - ) + store.receive(.authHandler(.failure(AuthHandlerError(id: 1) as NSError))) { + $0.authFailure = AuthHandlerError(id: 1).localizedDescription + } - store.receive(.authCallbacks(.handle( - .confirm(contact: contact, receptionId: Data(), ephemeralId: 0, roundId: 0) - ))) - XCTAssertNoDifference(dbDidSaveContact, [.init( - id: "id".data(using: .utf8)!, - authStatus: .friend, - createdAt: dbContact!.createdAt - )]) - - dbContact = nil - dbDidSaveContact = [] - didRegisterCallbacks.first?.handle( - .reset(contact: contact, receptionId: Data(), ephemeralId: 0, roundId: 0) - ) + store.send(.authHandler(.failureDismissed)) { + $0.authFailure = nil + } - store.receive(.authCallbacks(.handle( - .reset(contact: contact, receptionId: Data(), ephemeralId: 0, roundId: 0) - ))) - XCTAssertNoDifference(dbDidSaveContact, []) + store.send(.authHandler(.stop)) - store.send(.authCallbacks(.unregister)) + XCTAssertNoDifference(didCancelAuthHandler, 1) - XCTAssertNoDifference(didCancelAuthCallbacks, 1) + authHandlerOnError.first?(AuthHandlerError(id: 2)) } }