diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ResetAuthFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ResetAuthFeature.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..dded8f8def9e1bf44393cece148486be88e940d6 --- /dev/null +++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ResetAuthFeature.xcscheme @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1400" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "ResetAuthFeature" + BuildableName = "ResetAuthFeature" + BlueprintName = "ResetAuthFeature" + 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 = "ResetAuthFeatureTests" + BuildableName = "ResetAuthFeatureTests" + BlueprintName = "ResetAuthFeatureTests" + 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 = "ResetAuthFeature" + BuildableName = "ResetAuthFeature" + BlueprintName = "ResetAuthFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index d863e016e9f28069ddcc36104793639aef96be33..8e7e2984db8564b066fbe84cec6094b37838f6e3 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -25,6 +25,7 @@ let package = Package( .library(name: "HomeFeature", targets: ["HomeFeature"]), .library(name: "MyContactFeature", targets: ["MyContactFeature"]), .library(name: "RegisterFeature", targets: ["RegisterFeature"]), + .library(name: "ResetAuthFeature", targets: ["ResetAuthFeature"]), .library(name: "RestoreFeature", targets: ["RestoreFeature"]), .library(name: "SendRequestFeature", targets: ["SendRequestFeature"]), .library(name: "UserSearchFeature", targets: ["UserSearchFeature"]), @@ -95,6 +96,7 @@ let package = Package( .target(name: "HomeFeature"), .target(name: "MyContactFeature"), .target(name: "RegisterFeature"), + .target(name: "ResetAuthFeature"), .target(name: "RestoreFeature"), .target(name: "SendRequestFeature"), .target(name: "UserSearchFeature"), @@ -192,6 +194,7 @@ let package = Package( .target(name: "CheckContactAuthFeature"), .target(name: "ConfirmRequestFeature"), .target(name: "ContactLookupFeature"), + .target(name: "ResetAuthFeature"), .target(name: "SendRequestFeature"), .target(name: "VerifyContactFeature"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), @@ -310,6 +313,23 @@ let package = Package( ], swiftSettings: swiftSettings ), + .target( + name: "ResetAuthFeature", + dependencies: [ + .target(name: "AppCore"), + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), + .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), + ], + swiftSettings: swiftSettings + ), + .testTarget( + name: "ResetAuthFeatureTests", + dependencies: [ + .target(name: "ResetAuthFeature"), + ], + swiftSettings: swiftSettings + ), .target( name: "RestoreFeature", dependencies: [ diff --git a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme index 42a3cede69290ef2541f14fb20a7c54ad71831b5..09928db4491f32d54ece8ef3dce65bb252abbab7 100644 --- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme +++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme @@ -149,6 +149,16 @@ ReferencedContainer = "container:.."> </BuildableReference> </TestableReference> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "ResetAuthFeatureTests" + BuildableName = "ResetAuthFeatureTests" + BlueprintName = "ResetAuthFeatureTests" + ReferencedContainer = "container:.."> + </BuildableReference> + </TestableReference> <TestableReference skipped = "NO"> <BuildableReference diff --git a/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift index 729fe13eff8ee42ed1a12ba2ce03d082e5b2cfa8..a894b5e79200fb13b3986840410492050510e768 100644 --- a/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift +++ b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift @@ -18,7 +18,7 @@ extension AuthCallbackHandlerReset { guard var dbContact = try db().fetchContacts(.init(id: [id])).first else { return } - dbContact.authStatus = .stranger + dbContact.authStatus = .friend dbContact = try db().saveContact(dbContact) } } diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index ce52387f7624f3d3583f0be4e95571926327b754..fef073d1e4844f71b974de12c5c37ee7c988ba00 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -10,6 +10,7 @@ import Foundation import HomeFeature import MyContactFeature import RegisterFeature +import ResetAuthFeature import RestoreFeature import SendRequestFeature import UserSearchFeature @@ -77,6 +78,13 @@ extension AppEnvironment { bgQueue: bgQueue ) }, + resetAuth: { + ResetAuthEnvironment( + messenger: messenger, + mainQueue: mainQueue, + bgQueue: bgQueue + ) + }, chat: { ChatEnvironment( messenger: messenger, diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift index dc166e4ee49bf7d36d410caeb16589cf88be0439..be66fc8dfc8f59f7f2e20e1a1899a23da10106f6 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift @@ -6,6 +6,7 @@ import ComposablePresentation import ConfirmRequestFeature import ContactLookupFeature import Foundation +import ResetAuthFeature import SendRequestFeature import VerifyContactFeature import XCTestDynamicOverlay @@ -26,6 +27,7 @@ public struct ContactState: Equatable { verifyContact: VerifyContactState? = nil, confirmRequest: ConfirmRequestState? = nil, checkAuth: CheckContactAuthState? = nil, + resetAuth: ResetAuthState? = nil, chat: ChatState? = nil ) { self.id = id @@ -39,6 +41,7 @@ public struct ContactState: Equatable { self.verifyContact = verifyContact self.confirmRequest = confirmRequest self.checkAuth = checkAuth + self.resetAuth = resetAuth self.chat = chat } @@ -53,6 +56,7 @@ public struct ContactState: Equatable { public var verifyContact: VerifyContactState? public var confirmRequest: ConfirmRequestState? public var checkAuth: CheckContactAuthState? + public var resetAuth: ResetAuthState? public var chat: ChatState? } @@ -75,6 +79,9 @@ public enum ContactAction: Equatable, BindableAction { case confirmRequestTapped case confirmRequestDismissed case confirmRequest(ConfirmRequestAction) + case resetAuthTapped + case resetAuthDismissed + case resetAuth(ResetAuthAction) case chatTapped case chatDismissed case chat(ChatAction) @@ -92,6 +99,7 @@ public struct ContactEnvironment { verifyContact: @escaping () -> VerifyContactEnvironment, confirmRequest: @escaping () -> ConfirmRequestEnvironment, checkAuth: @escaping () -> CheckContactAuthEnvironment, + resetAuth: @escaping () -> ResetAuthEnvironment, chat: @escaping () -> ChatEnvironment ) { self.messenger = messenger @@ -103,6 +111,7 @@ public struct ContactEnvironment { self.verifyContact = verifyContact self.confirmRequest = confirmRequest self.checkAuth = checkAuth + self.resetAuth = resetAuth self.chat = chat } @@ -115,6 +124,7 @@ public struct ContactEnvironment { public var verifyContact: () -> VerifyContactEnvironment public var confirmRequest: () -> ConfirmRequestEnvironment public var checkAuth: () -> CheckContactAuthEnvironment + public var resetAuth: () -> ResetAuthEnvironment public var chat: () -> ChatEnvironment } @@ -130,6 +140,7 @@ extension ContactEnvironment { verifyContact: { .unimplemented }, confirmRequest: { .unimplemented }, checkAuth: { .unimplemented }, + resetAuth: { .unimplemented }, chat: { .unimplemented } ) } @@ -247,9 +258,21 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm state.chat = nil return .none + case .resetAuthTapped: + if let marshaled = state.dbContact?.marshaled { + state.resetAuth = ResetAuthState( + partner: .live(marshaled) + ) + } + return .none + + case .resetAuthDismissed: + state.resetAuth = nil + return .none + case .binding(_), .lookup(_), .sendRequest(_), .verifyContact(_), .confirmRequest(_), - .checkAuth(_), .chat(_): + .checkAuth(_), .resetAuth(_), .chat(_): return .none } } @@ -289,6 +312,13 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm action: /ContactAction.checkAuth, environment: { $0.checkAuth() } ) +.presenting( + resetAuthReducer, + state: .keyPath(\.resetAuth), + id: .notNil(), + action: /ContactAction.resetAuth, + environment: { $0.resetAuth() } +) .presenting( chatReducer, state: .keyPath(\.chat), diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index daa84903edacd947fb7173c3196e8ea7f5c1cf0d..7da763e03748536aabb0cc99698c2edd0ee0fba4 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -5,6 +5,7 @@ import ComposableArchitecture import ComposablePresentation import ConfirmRequestFeature import ContactLookupFeature +import ResetAuthFeature import SendRequestFeature import SwiftUI import VerifyContactFeature @@ -32,6 +33,7 @@ public struct ContactView: View { var canVerifyContact: Bool var canConfirmRequest: Bool var canCheckAuthorization: Bool + var canResetAuthorization: Bool init(state: ContactState) { dbContact = state.dbContact @@ -47,6 +49,7 @@ public struct ContactView: View { canVerifyContact = state.dbContact?.marshaled != nil canConfirmRequest = state.dbContact?.marshaled != nil canCheckAuthorization = state.dbContact?.marshaled != nil + canResetAuthorization = state.dbContact?.marshaled != nil } } @@ -178,6 +181,17 @@ public struct ContactView: View { } } .disabled(!viewStore.canCheckAuthorization) + + Button { + viewStore.send(.resetAuthTapped) + } label: { + HStack { + Text("Reset authorization") + Spacer() + Image(systemName: "chevron.forward") + } + } + .disabled(!viewStore.canResetAuthorization) } header: { Text("Auth") } @@ -242,6 +256,14 @@ public struct ContactView: View { onDeactivate: { viewStore.send(.checkAuthDismissed) }, destination: CheckContactAuthView.init(store:) )) + .background(NavigationLinkWithStore( + store.scope( + state: \.resetAuth, + action: ContactAction.resetAuth + ), + onDeactivate: { viewStore.send(.resetAuthDismissed) }, + destination: ResetAuthView.init(store:) + )) .background(NavigationLinkWithStore( store.scope( state: \.chat, diff --git a/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthFeature.swift b/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthFeature.swift new file mode 100644 index 0000000000000000000000000000000000000000..d4acb74002902f9b9a82da2981f78cd3312deb9e --- /dev/null +++ b/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthFeature.swift @@ -0,0 +1,90 @@ +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient + +public struct ResetAuthState: Equatable { + public init( + partner: Contact, + isResetting: Bool = false, + failure: String? = nil, + didReset: Bool = false + ) { + self.partner = partner + self.isResetting = isResetting + self.failure = failure + self.didReset = didReset + } + + public var partner: Contact + public var isResetting: Bool + public var failure: String? + public var didReset: Bool +} + +public enum ResetAuthAction: Equatable { + case resetTapped + case didReset + case didFail(String) +} + +public struct ResetAuthEnvironment { + public init( + messenger: Messenger, + mainQueue: AnySchedulerOf<DispatchQueue>, + bgQueue: AnySchedulerOf<DispatchQueue> + ) { + self.messenger = messenger + self.mainQueue = mainQueue + self.bgQueue = bgQueue + } + + public var messenger: Messenger + public var mainQueue: AnySchedulerOf<DispatchQueue> + public var bgQueue: AnySchedulerOf<DispatchQueue> +} + +#if DEBUG +extension ResetAuthEnvironment { + public static let unimplemented = ResetAuthEnvironment( + messenger: .unimplemented, + mainQueue: .unimplemented, + bgQueue: .unimplemented + ) +} +#endif + +public let resetAuthReducer = Reducer<ResetAuthState, ResetAuthAction, ResetAuthEnvironment> +{ state, action, env in + switch action { + case .resetTapped: + state.isResetting = true + state.didReset = false + state.failure = nil + return Effect.result { [state] in + do { + let e2e = try env.messenger.e2e.tryGet() + _ = try e2e.resetAuthenticatedChannel(partner: state.partner) + return .success(.didReset) + } catch { + return .success(.didFail(error.localizedDescription)) + } + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .didReset: + state.isResetting = false + state.didReset = true + state.failure = nil + return .none + + case .didFail(let failure): + state.isResetting = false + state.didReset = false + state.failure = failure + return .none + } +} diff --git a/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthView.swift b/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthView.swift new file mode 100644 index 0000000000000000000000000000000000000000..7b384efb74d841b4edf04a61cf55aa02a7c0d725 --- /dev/null +++ b/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthView.swift @@ -0,0 +1,80 @@ +import AppCore +import ComposableArchitecture +import SwiftUI + +public struct ResetAuthView: View { + public init(store: Store<ResetAuthState, ResetAuthAction>) { + self.store = store + } + + let store: Store<ResetAuthState, ResetAuthAction> + + struct ViewState: Equatable { + init(state: ResetAuthState) { + contactID = try? state.partner.getId() + isResetting = state.isResetting + failure = state.failure + didReset = state.didReset + } + + var contactID: Data? + var isResetting: Bool + var failure: String? + var didReset: Bool + } + + public var body: some View { + WithViewStore(store, observe: ViewState.init) { viewStore in + Form { + Section { + Text(viewStore.contactID?.hexString() ?? "") + .font(.footnote.monospaced()) + .textSelection(.enabled) + } header: { + Label("ID", systemImage: "number") + } + + Button { + viewStore.send(.resetTapped) + } label: { + HStack { + Text("Reset authenticated channel") + Spacer() + if viewStore.isResetting { + ProgressView() + } else if viewStore.didReset { + Image(systemName: "checkmark") + .foregroundColor(.green) + } + } + } + .disabled(viewStore.isResetting) + + if let failure = viewStore.failure { + Section { + Text(failure) + } header: { + Text("Error") + } + } + } + .navigationTitle("Reset auth") + } + } +} + +#if DEBUG +public struct ResetAuthView_Previews: PreviewProvider { + public static var previews: some View { + NavigationView { + ResetAuthView(store: Store( + initialState: ResetAuthState( + partner: .unimplemented(Data()) + ), + reducer: .empty, + environment: () + )) + } + } +} +#endif diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerResetTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerResetTests.swift index 8d07104652bf9412e678d4d09b9c215e99f0a2c8..a6273fba250c73de2e0cfe6700879fb5a1f15d58 100644 --- a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerResetTests.swift +++ b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerResetTests.swift @@ -10,8 +10,7 @@ final class AuthCallbackHandlerResetTests: XCTestCase { var didSaveContact: [XXModels.Contact] = [] let dbContact = XXModels.Contact( - id: "id".data(using: .utf8)!, - authStatus: .friend + id: "id".data(using: .utf8)! ) let reset = AuthCallbackHandlerReset.live( db: .init { @@ -34,7 +33,7 @@ final class AuthCallbackHandlerResetTests: XCTestCase { XCTAssertNoDifference(didFetchContacts, [.init(id: ["id".data(using: .utf8)!])]) var expectedSavedContact = dbContact - expectedSavedContact.authStatus = .stranger + expectedSavedContact.authStatus = .friend XCTAssertNoDifference(didSaveContact, [expectedSavedContact]) } diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift index c200094f49e9164391cdd20afdc53ed793a9c736..eba41b7ab57d1dddfc0827747760a9bce224bdab 100644 --- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift @@ -5,6 +5,7 @@ import ComposableArchitecture import ConfirmRequestFeature import ContactLookupFeature import CustomDump +import ResetAuthFeature import SendRequestFeature import VerifyContactFeature import XCTest @@ -294,6 +295,44 @@ final class ContactFeatureTests: XCTestCase { } } + func testResetAuthTapped() { + let contactData = "contact-data".data(using: .utf8)! + let store = TestStore( + initialState: ContactState( + id: Data(), + dbContact: XXModels.Contact( + id: Data(), + marshaled: contactData + ) + ), + reducer: contactReducer, + environment: .unimplemented + ) + + store.send(.resetAuthTapped) { + $0.resetAuth = ResetAuthState( + partner: .unimplemented(contactData) + ) + } + } + + func testResetAuthDismissed() { + let store = TestStore( + initialState: ContactState( + id: Data(), + resetAuth: ResetAuthState( + partner: .unimplemented(Data()) + ) + ), + reducer: contactReducer, + environment: .unimplemented + ) + + store.send(.resetAuthDismissed) { + $0.resetAuth = nil + } + } + func testConfirmRequestTapped() { let contactData = "contact-data".data(using: .utf8)! let store = TestStore( diff --git a/Examples/xx-messenger/Tests/ResetAuthFeatureTests/ResetAuthFeatureTests.swift b/Examples/xx-messenger/Tests/ResetAuthFeatureTests/ResetAuthFeatureTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..46b5dc9e402d5c2cb71a796790b205732876a658 --- /dev/null +++ b/Examples/xx-messenger/Tests/ResetAuthFeatureTests/ResetAuthFeatureTests.swift @@ -0,0 +1,72 @@ +import ComposableArchitecture +import CustomDump +import XCTest +import XXClient +@testable import ResetAuthFeature + +final class ResetAuthFeatureTests: XCTestCase { + func testReset() { + let partnerData = "contact-data".data(using: .utf8)! + let partner = Contact.unimplemented(partnerData) + + var didResetAuthChannel: [Contact] = [] + + let store = TestStore( + initialState: ResetAuthState( + partner: partner + ), + reducer: resetAuthReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.resetAuthenticatedChannel.run = { contact in + didResetAuthChannel.append(contact) + return 0 + } + return e2e + } + + store.send(.resetTapped) { + $0.isResetting = true + } + + XCTAssertNoDifference(didResetAuthChannel, [partner]) + + store.receive(.didReset) { + $0.isResetting = false + $0.didReset = true + } + } + + func testResetFailure() { + struct Failure: Error {} + let failure = Failure() + + let store = TestStore( + initialState: ResetAuthState( + partner: .unimplemented(Data()) + ), + reducer: resetAuthReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.resetAuthenticatedChannel.run = { _ in throw failure } + return e2e + } + + store.send(.resetTapped) { + $0.isResetting = true + } + + store.receive(.didFail(failure.localizedDescription)) { + $0.isResetting = false + $0.failure = failure.localizedDescription + } + } +}