diff --git a/Networking/Networking/Remote/AccountRemote.swift b/Networking/Networking/Remote/AccountRemote.swift index 1ddbf03745f..df701be919c 100644 --- a/Networking/Networking/Remote/AccountRemote.swift +++ b/Networking/Networking/Remote/AccountRemote.swift @@ -13,7 +13,7 @@ public protocol AccountRemoteProtocol { func checkIfWooCommerceIsActive(for siteID: Int64) -> AnyPublisher, Never> func fetchWordPressSiteSettings(for siteID: Int64) -> AnyPublisher, Never> func loadSitePlan(for siteID: Int64, completion: @escaping (Result) -> Void) - func loadUsernameSuggestions(from text: String) async -> Result<[String], Error> + func loadUsernameSuggestions(from text: String) async throws -> [String] /// Creates a WPCOM account with the given email and password. /// - Parameters: @@ -131,17 +131,15 @@ public class AccountRemote: Remote, AccountRemoteProtocol { enqueue(request, mapper: mapper, completion: completion) } - public func loadUsernameSuggestions(from text: String) async -> Result<[String], Error> { + public func loadUsernameSuggestions(from text: String) async throws -> [String] { let path = Path.usernameSuggestions let parameters = [ParameterKey.name: text] let request = DotcomRequest(wordpressApiVersion: .wpcomMark2, method: .get, path: path, parameters: parameters) - do { - let result: [String: [String]] = try await enqueue(request) - let suggestions = result["suggestions"] ?? [] - return .success(suggestions) - } catch { - return .failure(error) - } + + let result: [String: [String]] = try await enqueue(request) + let suggestions = result["suggestions"] ?? [] + + return suggestions } public func createAccount(email: String, diff --git a/Networking/Networking/Remote/DomainRemote.swift b/Networking/Networking/Remote/DomainRemote.swift index 309d890d80e..df9b074b912 100644 --- a/Networking/Networking/Remote/DomainRemote.swift +++ b/Networking/Networking/Remote/DomainRemote.swift @@ -5,13 +5,13 @@ public protocol DomainRemoteProtocol { /// Loads domain suggestions that are free (`*.wordpress.com` only) based on the query. /// - Parameter query: What the domain suggestions are based on. /// - Returns: The result of free domain suggestions. - func loadFreeDomainSuggestions(query: String) async -> Result<[FreeDomainSuggestion], Error> + func loadFreeDomainSuggestions(query: String) async throws -> [FreeDomainSuggestion] } /// Domain: Remote Endpoints /// public class DomainRemote: Remote, DomainRemoteProtocol { - public func loadFreeDomainSuggestions(query: String) async -> Result<[FreeDomainSuggestion], Error> { + public func loadFreeDomainSuggestions(query: String) async throws -> [FreeDomainSuggestion] { let path = Path.domainSuggestions let parameters: [String: Any] = [ ParameterKey.query: query, @@ -19,12 +19,7 @@ public class DomainRemote: Remote, DomainRemoteProtocol { ParameterKey.wordPressDotComSubdomainsOnly: true ] let request = DotcomRequest(wordpressApiVersion: .mark1_1, method: .get, path: path, parameters: parameters) - do { - let suggestions: [FreeDomainSuggestion] = try await enqueue(request) - return .success(suggestions) - } catch { - return .failure(error) - } + return try await enqueue(request) } } diff --git a/Networking/Networking/Remote/JustInTimeMessagesRemote.swift b/Networking/Networking/Remote/JustInTimeMessagesRemote.swift index 106cc1b170c..3f316603c21 100644 --- a/Networking/Networking/Remote/JustInTimeMessagesRemote.swift +++ b/Networking/Networking/Remote/JustInTimeMessagesRemote.swift @@ -4,10 +4,10 @@ public protocol JustInTimeMessagesRemoteProtocol { func loadAllJustInTimeMessages(for siteID: Int64, messagePath: JustInTimeMessagesRemote.MessagePath, query: [String: String?]?, - locale: String?) async -> Result<[JustInTimeMessage], Error> + locale: String?) async throws -> [JustInTimeMessage] func dismissJustInTimeMessage(for siteID: Int64, messageID: String, - featureClass: String) async -> Result + featureClass: String) async throws -> Bool } /// Just In Time Messages: Remote endpoints @@ -28,7 +28,7 @@ public final class JustInTimeMessagesRemote: Remote, JustInTimeMessagesRemotePro public func loadAllJustInTimeMessages(for siteID: Int64, messagePath: JustInTimeMessagesRemote.MessagePath, query: [String: String?]?, - locale: String?) async -> Result<[JustInTimeMessage], Error> { + locale: String?) async throws -> [JustInTimeMessage] { let request = JetpackRequest(wooApiVersion: .none, method: .get, siteID: siteID, @@ -39,7 +39,7 @@ public final class JustInTimeMessagesRemote: Remote, JustInTimeMessagesRemotePro let mapper = JustInTimeMessageListMapper(siteID: siteID) - return await enqueue(request, mapper: mapper) + return try await enqueue(request, mapper: mapper) } private func getParameters(messagePath: JustInTimeMessagesRemote.MessagePath, @@ -88,7 +88,7 @@ public final class JustInTimeMessagesRemote: Remote, JustInTimeMessagesRemotePro /// public func dismissJustInTimeMessage(for siteID: Int64, messageID: String, - featureClass: String) async -> Result { + featureClass: String) async throws -> Bool { let parameters = [ParameterKey.featureClass: featureClass, ParameterKey.messageID: messageID] @@ -99,7 +99,7 @@ public final class JustInTimeMessagesRemote: Remote, JustInTimeMessagesRemotePro path: Path.jitm, parameters: parameters) - return await enqueue(request, mapper: DataBoolMapper()) + return try await enqueue(request, mapper: DataBoolMapper()) } } diff --git a/Networking/Networking/Remote/Remote.swift b/Networking/Networking/Remote/Remote.swift index f3897e7f6ed..9917c3f2fbc 100644 --- a/Networking/Networking/Remote/Remote.swift +++ b/Networking/Networking/Remote/Remote.swift @@ -201,9 +201,9 @@ public class Remote: NSObject { /// /// - Parameter request: Request that should be performed. /// - Returns: The result from the JSON parsed response for the expected type. - func enqueue(_ request: Request, mapper: M) async -> Result { - await withCheckedContinuation { continuation in - network.responseData(for: request) { [weak self] result in + func enqueue(_ request: Request, mapper: M) async throws -> M.Output { + try await withCheckedThrowingContinuation { continuation in + network.responseData(for: request) { [weak self] (result: Swift.Result) in guard let self else { return } switch result { @@ -212,14 +212,14 @@ public class Remote: NSObject { let validator = request.responseDataValidator() try validator.validate(data: data) let parsed = try mapper.map(response: data) - continuation.resume(returning: .success(parsed)) + continuation.resume(returning: parsed) } catch { DDLogError("<> Mapping Error: \(error)") self.handleResponseError(error: error, for: request) - continuation.resume(returning: .failure(error)) + continuation.resume(throwing: error) } case .failure(let error): - continuation.resume(returning: .failure(error)) + continuation.resume(throwing: error) } } } diff --git a/Networking/Networking/Remote/SiteRemote.swift b/Networking/Networking/Remote/SiteRemote.swift index 9aa75923323..e723f5568e1 100644 --- a/Networking/Networking/Remote/SiteRemote.swift +++ b/Networking/Networking/Remote/SiteRemote.swift @@ -6,9 +6,8 @@ public protocol SiteRemoteProtocol { /// - Parameters: /// - name: The name of the site. /// - domain: The domain selected for the site. - /// - Returns: The result of site creation. - func createSite(name: String, - domain: String) async -> Result + /// - Returns: The response with the site creation. + func createSite(name: String, domain: String) async throws -> SiteCreationResponse } /// Site: Remote Endpoints @@ -24,12 +23,12 @@ public class SiteRemote: Remote, SiteRemoteProtocol { } public func createSite(name: String, - domain: String) async -> Result { + domain: String) async throws -> SiteCreationResponse { let path = Path.siteCreation // Domain input should be a `wordpress.com` subdomain. guard let subdomainName = domain.split(separator: ".").first else { - return .failure(SiteCreationError.invalidDomain) + throw SiteCreationError.invalidDomain } let parameters: [String: Any] = [ "blog_name": subdomainName, @@ -52,12 +51,7 @@ public class SiteRemote: Remote, SiteRemoteProtocol { ] let request = DotcomRequest(wordpressApiVersion: .mark1_1, method: .post, path: path, parameters: parameters) - do { - let response: SiteCreationResponse = try await enqueue(request) - return .success(response) - } catch { - return .failure(error) - } + return try await enqueue(request) } } diff --git a/Networking/NetworkingTests/Remote/AccountRemoteTests.swift b/Networking/NetworkingTests/Remote/AccountRemoteTests.swift index 142b09ce252..9c1aab1f5ad 100644 --- a/Networking/NetworkingTests/Remote/AccountRemoteTests.swift +++ b/Networking/NetworkingTests/Remote/AccountRemoteTests.swift @@ -1,5 +1,6 @@ import Combine import XCTest +import TestKit @testable import Networking @@ -151,10 +152,9 @@ final class AccountRemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "username/suggestions", filename: "account-username-suggestions") // When - let result = await remote.loadUsernameSuggestions(from: "woo") + let suggestions = try await remote.loadUsernameSuggestions(from: "woo") // Then - let suggestions = try XCTUnwrap(result.get()) XCTAssertEqual(suggestions, ["woowriter", "woowoowoo", "woodaily"]) } @@ -162,12 +162,7 @@ final class AccountRemoteTests: XCTestCase { // Given let remote = AccountRemote(network: network) - // When - let result = await remote.loadUsernameSuggestions(from: "woo") - - // Then - let error = try XCTUnwrap(result.failure as? NetworkError) - XCTAssertEqual(error, .notFound) + await assertThrowsError({ _ = try await remote.loadUsernameSuggestions(from: "woo")}, errorAssert: { ($0 as? NetworkError) == .notFound }) } // MARK: - `createAccount` diff --git a/Networking/NetworkingTests/Remote/DomainRemoteTests.swift b/Networking/NetworkingTests/Remote/DomainRemoteTests.swift index a05cdb23225..fb3559438d1 100644 --- a/Networking/NetworkingTests/Remote/DomainRemoteTests.swift +++ b/Networking/NetworkingTests/Remote/DomainRemoteTests.swift @@ -1,4 +1,5 @@ import XCTest +import TestKit @testable import Networking final class DomainRemoteTests: XCTestCase { @@ -21,11 +22,9 @@ final class DomainRemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "domains/suggestions", filename: "domain-suggestions") // When - let result = await remote.loadFreeDomainSuggestions(query: "domain") + let suggestions = try await remote.loadFreeDomainSuggestions(query: "domain") // Then - XCTAssertTrue(result.isSuccess) - let suggestions = try XCTUnwrap(result.get()) XCTAssertEqual(suggestions, [ .init(name: "domaintestingtips.wordpress.com", isFree: true), .init(name: "domaintestingtoday.wordpress.com", isFree: true), @@ -36,12 +35,6 @@ final class DomainRemoteTests: XCTestCase { // Given let remote = DomainRemote(network: network) - // When - let result = await remote.loadFreeDomainSuggestions(query: "domain") - - // Then - XCTAssertTrue(result.isFailure) - let error = try XCTUnwrap(result.failure as? NetworkError) - XCTAssertEqual(error, .notFound) + await assertThrowsError({_ = try await remote.loadFreeDomainSuggestions(query: "domain")}, errorAssert: { ($0 as? NetworkError) == .notFound }) } } diff --git a/Networking/NetworkingTests/Remote/JustInTimeMessagesRemoteTests.swift b/Networking/NetworkingTests/Remote/JustInTimeMessagesRemoteTests.swift index 240d9fa1354..bfda19a0f7a 100644 --- a/Networking/NetworkingTests/Remote/JustInTimeMessagesRemoteTests.swift +++ b/Networking/NetworkingTests/Remote/JustInTimeMessagesRemoteTests.swift @@ -27,7 +27,7 @@ final class JustInTimeMessagesRemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "jetpack/v4/jitm", filename: "just-in-time-message-list") // When - let result = await remote.loadAllJustInTimeMessages( + let justInTimeMessages = try await remote.loadAllJustInTimeMessages( for: self.sampleSiteID, messagePath: JustInTimeMessagesRemote.MessagePath( app: .wooMobile, @@ -37,8 +37,6 @@ final class JustInTimeMessagesRemoteTests: XCTestCase { locale: "en_US") // Then - XCTAssert(result.isSuccess) - let justInTimeMessages = try XCTUnwrap(result.get()) assertEqual(1, justInTimeMessages.count) } @@ -49,7 +47,7 @@ final class JustInTimeMessagesRemoteTests: XCTestCase { let remote = JustInTimeMessagesRemote(network: network) // When - _ = await remote.loadAllJustInTimeMessages( + _ = try? await remote.loadAllJustInTimeMessages( for: self.sampleSiteID, messagePath: JustInTimeMessagesRemote.MessagePath( app: .wooMobile, @@ -72,7 +70,7 @@ final class JustInTimeMessagesRemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "jetpack/v4/jitm", filename: "just-in-time-message-list") // When - let result = await remote.loadAllJustInTimeMessages( + let justInTimeMessages = try await remote.loadAllJustInTimeMessages( for: self.sampleSiteID, messagePath: JustInTimeMessagesRemote.MessagePath( app: .wooMobile, @@ -82,7 +80,6 @@ final class JustInTimeMessagesRemoteTests: XCTestCase { locale: "en_US") // Then - let justInTimeMessages = try result.get() assertEqual(sampleSiteID, justInTimeMessages.first?.siteID) } @@ -93,7 +90,7 @@ final class JustInTimeMessagesRemoteTests: XCTestCase { let remote = JustInTimeMessagesRemote(network: network) // When - _ = await remote.loadAllJustInTimeMessages( + _ = try? await remote.loadAllJustInTimeMessages( for: self.sampleSiteID, messagePath: JustInTimeMessagesRemote.MessagePath( app: .wooMobile, @@ -116,23 +113,20 @@ final class JustInTimeMessagesRemoteTests: XCTestCase { // Given let remote = JustInTimeMessagesRemote(network: network) - let error = NetworkError.unacceptableStatusCode(statusCode: 403) - network.simulateError(requestUrlSuffix: "jetpack/v4/jitm", error: error) + let expectedError = NetworkError.unacceptableStatusCode(statusCode: 403) + network.simulateError(requestUrlSuffix: "jetpack/v4/jitm", error: expectedError) // When - let result = await remote.loadAllJustInTimeMessages( - for: self.sampleSiteID, - messagePath: JustInTimeMessagesRemote.MessagePath( - app: .wooMobile, - screen: "my_store", - hook: .adminNotices), - query: nil, - locale: "en_US") - - // Then - XCTAssertTrue(result.isFailure) - let resultError = try XCTUnwrap(result.failure as? NetworkError) - assertEqual(error, resultError) + await assertThrowsError({ + _ = try await remote.loadAllJustInTimeMessages( + for: self.sampleSiteID, + messagePath: JustInTimeMessagesRemote.MessagePath( + app: .wooMobile, + screen: "my_store", + hook: .adminNotices), + query: nil, + locale: "en_US") + }, errorAssert: { ($0 as? NetworkError) == expectedError }) } func test_test_loadAllJustInTimeMessages_uses_passed_locale_for_request() async throws { @@ -140,7 +134,7 @@ final class JustInTimeMessagesRemoteTests: XCTestCase { let remote = JustInTimeMessagesRemote(network: network) // When - _ = await remote.loadAllJustInTimeMessages( + _ = try? await remote.loadAllJustInTimeMessages( for: self.sampleSiteID, messagePath: JustInTimeMessagesRemote.MessagePath( app: .wooMobile, @@ -162,7 +156,7 @@ final class JustInTimeMessagesRemoteTests: XCTestCase { let remote = JustInTimeMessagesRemote(network: network) // When - _ = await remote.loadAllJustInTimeMessages( + _ = try? await remote.loadAllJustInTimeMessages( for: self.sampleSiteID, messagePath: JustInTimeMessagesRemote.MessagePath( app: .wooMobile, @@ -180,7 +174,6 @@ final class JustInTimeMessagesRemoteTests: XCTestCase { let queryItems = try XCTUnwrap(URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems) let queryJson = try XCTUnwrap(queryItems.first { $0.name == "query" }?.value) assertThat(queryJson, contains: "\"query\":") - let parameters = request.parameters let jitmQuery = try XCTUnwrap(request.parameters["query"] as? String) // Individually check query items because dictionaries aren't ordered assertThat(jitmQuery, contains: "platform=ios") // platform=ios diff --git a/Networking/NetworkingTests/Remote/RemoteTests.swift b/Networking/NetworkingTests/Remote/RemoteTests.swift index 3c7e90c9bb5..0e2eb0d71cc 100644 --- a/Networking/NetworkingTests/Remote/RemoteTests.swift +++ b/Networking/NetworkingTests/Remote/RemoteTests.swift @@ -1,6 +1,7 @@ import Combine import XCTest import Fakes +import TestKit @testable import Networking @@ -275,12 +276,7 @@ final class RemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "mock", filename: "timeout_error") - // When - let result = await remote.enqueue(request, mapper: mapper) - - // Then - let error = try XCTUnwrap(result.failure) - XCTAssert(error is DotcomError) + await assertThrowsError({ _ = try await remote.enqueue(request, mapper: mapper)}, errorAssert: { $0 is DotcomError }) } /// Verifies that dotcom v1.1 request doesn't parse WordPressApiError @@ -295,10 +291,8 @@ final class RemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "mock", filename: "error-wp-rest-forbidden") // When - let result = await remote.enqueue(request, mapper: mapper) - - // Then - XCTAssert(result.isSuccess) + let result = try await remote.enqueue(request, mapper: mapper) + XCTAssertNotNil(result) } /// Verifies that dotcom v1.2 request parses DotcomError @@ -312,12 +306,7 @@ final class RemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "mock", filename: "timeout_error") - // When - let result = await remote.enqueue(request, mapper: mapper) - - // Then - let error = try XCTUnwrap(result.failure) - XCTAssert(error is DotcomError) + await assertThrowsError({ _ = try await remote.enqueue(request, mapper: mapper)}, errorAssert: { $0 is DotcomError }) } /// Verifies that dotcom v1.2 request doesn't parse WordPressApiError @@ -332,10 +321,10 @@ final class RemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "mock", filename: "error-wp-rest-forbidden") // When - let result = await remote.enqueue(request, mapper: mapper) + let result = try await remote.enqueue(request, mapper: mapper) // Then - XCTAssert(result.isSuccess) + XCTAssertNotNil(result) } /// Verifies that dotcom wpcom v2 request parses WordPressApiError @@ -349,12 +338,7 @@ final class RemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "mock", filename: "error-wp-rest-forbidden") - // When - let result = await remote.enqueue(request, mapper: mapper) - - // Then - let error = try XCTUnwrap(result.failure) - XCTAssert(error is WordPressApiError) + await assertThrowsError({ _ = try await remote.enqueue(request, mapper: mapper)}, errorAssert: { $0 is WordPressApiError }) } /// Verifies that dotcom wpcom v2 request doesn't parse DotcomError @@ -369,10 +353,10 @@ final class RemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "mock", filename: "timeout_error") // When - let result = await remote.enqueue(request, mapper: mapper) + let result = try await remote.enqueue(request, mapper: mapper) // Then - XCTAssert(result.isSuccess) + XCTAssertNotNil(result) } /// Verifies that dotcom wp v2 request parses WordPressApiError @@ -386,12 +370,7 @@ final class RemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "mock", filename: "error-wp-rest-forbidden") - // When - let result = await remote.enqueue(request, mapper: mapper) - - // Then - let error = try XCTUnwrap(result.failure) - XCTAssert(error is WordPressApiError) + await assertThrowsError({ _ = try await remote.enqueue(request, mapper: mapper)}, errorAssert: { $0 is WordPressApiError }) } /// Verifies that dotcom wp v2 request doesn't parse DotcomError @@ -406,10 +385,10 @@ final class RemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "mock", filename: "timeout_error") // When - let result = await remote.enqueue(request, mapper: mapper) + let result = try await remote.enqueue(request, mapper: mapper) // Then - XCTAssert(result.isSuccess) + XCTAssertNotNil(result) } /// Verifies that Jetpack request parses DotcomError @@ -423,12 +402,7 @@ final class RemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "mock", filename: "timeout_error") - // When - let result = await remote.enqueue(request, mapper: mapper) - - // Then - let error = try XCTUnwrap(result.failure) - XCTAssert(error is DotcomError) + await assertThrowsError({ _ = try await remote.enqueue(request, mapper: mapper)}, errorAssert: { $0 is DotcomError }) } /// Verifies that Jetpack request doesn't parse WordPressApiError @@ -443,10 +417,10 @@ final class RemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "mock", filename: "error-wp-rest-forbidden") // When - let result = await remote.enqueue(request, mapper: mapper) + let result = try await remote.enqueue(request, mapper: mapper) // Then - XCTAssert(result.isSuccess) + XCTAssertNotNil(result) } /// Verifies that WordPressOrg request parses WordPressApiError @@ -460,12 +434,7 @@ final class RemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "mock", filename: "error-wp-rest-forbidden") - // When - let result = await remote.enqueue(request, mapper: mapper) - - // Then - let error = try XCTUnwrap(result.failure) - XCTAssert(error is WordPressApiError) + await assertThrowsError({ _ = try await remote.enqueue(request, mapper: mapper)}, errorAssert: { $0 is WordPressApiError }) } /// Verifies that WordPressOrg request doesn't parse DotcomError @@ -480,10 +449,10 @@ final class RemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "mock", filename: "timeout_error") // When - let result = await remote.enqueue(request, mapper: mapper) + let result = try await remote.enqueue(request, mapper: mapper) // Then - XCTAssert(result.isSuccess) + XCTAssertNotNil(result) } } diff --git a/Networking/NetworkingTests/Remote/SiteRemoteTests.swift b/Networking/NetworkingTests/Remote/SiteRemoteTests.swift index 01a6d857419..828a2026980 100644 --- a/Networking/NetworkingTests/Remote/SiteRemoteTests.swift +++ b/Networking/NetworkingTests/Remote/SiteRemoteTests.swift @@ -1,4 +1,5 @@ import XCTest +import TestKit @testable import Networking final class SiteRemoteTests: XCTestCase { @@ -24,11 +25,9 @@ final class SiteRemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "sites/new", filename: "site-creation-success") // When - let result = await remote.createSite(name: "Wapuu swags", domain: "wapuu.store") + let response = try await remote.createSite(name: "Wapuu swags", domain: "wapuu.store") // Then - XCTAssertTrue(result.isSuccess) - let response = try XCTUnwrap(result.get()) XCTAssertTrue(response.success) XCTAssertEqual(response.site, .init(siteID: "202211", name: "Wapuu swags", @@ -37,34 +36,26 @@ final class SiteRemoteTests: XCTestCase { } func test_createSite_returns_invalidDomain_error_when_domain_is_empty() async throws { - // When - let result = await remote.createSite(name: "Wapuu swags", domain: "") - - // Then - let error = try XCTUnwrap(result.failure as? SiteCreationError) - XCTAssertEqual(error, .invalidDomain) + await assertThrowsError({ _ = try await remote.createSite(name: "Wapuu swags", domain: "") }, + errorAssert: { ($0 as? SiteCreationError) == .invalidDomain} ) } func test_createSite_returns_DotcomError_failure_on_domain_error() async throws { // Given network.simulateResponse(requestUrlSuffix: "sites/new", filename: "site-creation-domain-error") - // When - let result = await remote.createSite(name: "Wapuu swags", domain: "wapuu.store") - - // Then - let error = try XCTUnwrap(result.failure as? DotcomError) - XCTAssertEqual(error, - .unknown(code: "blog_name_only_lowercase_letters_and_numbers", - message: "Site names can only contain lowercase letters (a-z) and numbers.")) + await assertThrowsError({ + // When + _ = try await remote.createSite(name: "Wapuu swags", domain: "wapuu.store") + }, errorAssert: { ($0 as? DotcomError) == .unknown(code: "blog_name_only_lowercase_letters_and_numbers", + message: "Site names can only contain lowercase letters (a-z) and numbers.") + }) } func test_createSite_returns_failure_on_empty_response() async throws { - // When - let result = await remote.createSite(name: "Wapuu swags", domain: "wapuu.store") - - // Then - let error = try XCTUnwrap(result.failure as? NetworkError) - XCTAssertEqual(error, .notFound) + await assertThrowsError({ + // When + _ = try await remote.createSite(name: "Wapuu swags", domain: "wapuu.store") + }, errorAssert: { ($0 as? NetworkError) == .notFound }) } } diff --git a/TestKit/Sources/TestKit/Assertions.swift b/TestKit/Sources/TestKit/Assertions.swift index f896915fc9e..5cc653f45ab 100644 --- a/TestKit/Sources/TestKit/Assertions.swift +++ b/TestKit/Sources/TestKit/Assertions.swift @@ -49,6 +49,22 @@ public func assertThat(_ subject: Any?, isAnInstanceOf expectedType: T.Type, line: line) } +/// Asserts that the async throws `expression` throws an error, and asserts the given Bool expression +/// with the generated error. +/// +public func assertThrowsError(_ expression: () async throws -> (), errorAssert: (Error) -> Bool, file: StaticString = #file, line: UInt = #line) async { + do { + _ = try await expression() + XCTFail("It should throw an error", + file: file, + line: line) + } catch { + XCTAssert(errorAssert(error), + file: file, + line: line) + } +} + extension XCTestCase { /// Alternative to the regular `XCTAssertEqual` that outputs a `diff` between the `expect` and `received` objects. /// diff --git a/WooCommerce/Classes/Authentication/Epilogue/StorePickerViewController.swift b/WooCommerce/Classes/Authentication/Epilogue/StorePickerViewController.swift index 348fa6a739f..66c1a35e8cf 100644 --- a/WooCommerce/Classes/Authentication/Epilogue/StorePickerViewController.swift +++ b/WooCommerce/Classes/Authentication/Epilogue/StorePickerViewController.swift @@ -157,8 +157,8 @@ final class StorePickerViewController: UIViewController { private lazy var removeAppleIDAccessCoordinator: RemoveAppleIDAccessCoordinator = RemoveAppleIDAccessCoordinator(sourceViewController: self) { [weak self] in - guard let self = self else { return .failure(RemoveAppleIDAccessError.presenterDeallocated) } - return await self.removeAppleIDAccess() + guard let self = self else { throw RemoveAppleIDAccessError.presenterDeallocated } + return try await self.removeAppleIDAccess() } onRemoveSuccess: { [weak self] in self?.restartAuthentication() } @@ -758,11 +758,11 @@ extension StorePickerViewController: UITableViewDelegate { } private extension StorePickerViewController { - func removeAppleIDAccess() async -> Result { - await withCheckedContinuation { [weak self] continuation in + func removeAppleIDAccess() async throws { + try await withCheckedThrowingContinuation { [weak self] continuation in guard let self = self else { return } let action = AccountAction.closeAccount { result in - continuation.resume(returning: result) + continuation.resume(with: result) } self.stores.dispatch(action) } diff --git a/WooCommerce/Classes/ViewModels/Authentication/AccountCreationFormViewModel.swift b/WooCommerce/Classes/ViewModels/Authentication/AccountCreationFormViewModel.swift index 358f29142ad..f2e81b56d1d 100644 --- a/WooCommerce/Classes/ViewModels/Authentication/AccountCreationFormViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Authentication/AccountCreationFormViewModel.swift @@ -52,27 +52,25 @@ final class AccountCreationFormViewModel: ObservableObject { /// Creates a WPCOM account with the email and password. /// - Returns: async result of account creation. @MainActor - func createAccount() async -> Result { + func createAccount() async throws { analytics.track(event: .StoreCreation.signupSubmitted()) - let result: Result = await withCheckedContinuation { continuation in - let action = AccountCreationAction.createAccount(email: email, password: password) { result in - continuation.resume(returning: result) + do { + let data = try await withCheckedThrowingContinuation { continuation in + let action = AccountCreationAction.createAccount(email: email, password: password) { result in + continuation.resume(with: result) + } + stores.dispatch(action) } - stores.dispatch(action) - } - switch result { - case .success(let data): analytics.track(event: .StoreCreation.signupSuccess()) await handleSuccess(data: data) - return .success(()) - case .failure(let error): + } catch let error as CreateAccountError { analytics.track(event: .StoreCreation.signupFailed(error: error)) - handleFailure(error: error) - return .failure(error) + + throw error } } } diff --git a/WooCommerce/Classes/ViewRelated/Authentication/AccountCreationForm.swift b/WooCommerce/Classes/ViewRelated/Authentication/AccountCreationForm.swift index 54e84bf8c23..3418be00c33 100644 --- a/WooCommerce/Classes/ViewRelated/Authentication/AccountCreationForm.swift +++ b/WooCommerce/Classes/ViewRelated/Authentication/AccountCreationForm.swift @@ -122,14 +122,11 @@ struct AccountCreationForm: View { // CTA to submit the form. Button(Localization.submitButtonTitle) { Task { @MainActor in - isPerformingTask = true - let result = await viewModel.createAccount() + let createAccountCompleted = (try? await viewModel.createAccount()) != nil isPerformingTask = false - switch result { - case .success: + + if createAccountCompleted { completion() - case .failure: - break } } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorViewModel.swift index 5ebc0a47004..59727a22b7d 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorViewModel.swift @@ -51,13 +51,13 @@ private extension DomainSelectorViewModel { Task { @MainActor in self.errorMessage = nil self.isLoadingDomainSuggestions = true - let result = await self.loadFreeDomainSuggestions(query: searchTerm) - self.isLoadingDomainSuggestions = false - switch result { - case .success(let suggestions): + do { + let suggestions = try await self.loadFreeDomainSuggestions(query: searchTerm) + self.isLoadingDomainSuggestions = false self.handleFreeDomainSuggestions(suggestions, query: searchTerm) - case .failure(let error): + } catch { + self.isLoadingDomainSuggestions = false self.handleError(error) } } @@ -65,10 +65,10 @@ private extension DomainSelectorViewModel { } @MainActor - func loadFreeDomainSuggestions(query: String) async -> Result<[FreeDomainSuggestion], Error> { - await withCheckedContinuation { continuation in + func loadFreeDomainSuggestions(query: String) async throws -> [FreeDomainSuggestion] { + try await withCheckedThrowingContinuation { continuation in let action = DomainAction.loadFreeDomainSuggestions(query: searchTerm) { result in - continuation.resume(returning: result) + continuation.resume(with: result) } stores.dispatch(action) } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/RemoveAppleIDAccessCoordinator.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/RemoveAppleIDAccessCoordinator.swift index 536e09177be..70823db5401 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/RemoveAppleIDAccessCoordinator.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/RemoveAppleIDAccessCoordinator.swift @@ -4,7 +4,7 @@ import protocol Yosemite.StoresManager /// Coordinates navigation for removing Apple ID access from various entry points (settings and empty stores screens). @MainActor final class RemoveAppleIDAccessCoordinator { private let sourceViewController: UIViewController - private let removeAction: () async -> Result + private let removeAction: () async throws -> Void private let onRemoveSuccess: () -> Void private let stores: StoresManager private let analytics: Analytics @@ -14,7 +14,7 @@ import protocol Yosemite.StoresManager /// - removeAction: called when the remove action is confirmed. /// - onRemoveSuccess: called when the removal is successful. init(sourceViewController: UIViewController, - removeAction: @escaping () async -> Result, + removeAction: @escaping () async throws -> Void, onRemoveSuccess: @escaping () -> Void, stores: StoresManager = ServiceLocator.stores, analytics: Analytics = ServiceLocator.analytics) { @@ -50,19 +50,24 @@ private extension RemoveAppleIDAccessCoordinator { Task { @MainActor [weak self] in guard let self = self else { return } - let result = await self.removeAction() - self.dismissInProgressUI { [weak self] in - guard let self = self else { return } - switch result { - case .success: - self.analytics.track(event: .closeAccountSuccess()) - self.onRemoveSuccess() - case .failure(let error): - self.analytics.track(event: .closeAccountFailed(error: error)) + + var dismissInProgressUICompletion: () -> Void + + do { + try await self.removeAction() + dismissInProgressUICompletion = { [weak self] in + self?.analytics.track(event: .closeAccountSuccess()) + self?.onRemoveSuccess() + } + } catch { + dismissInProgressUICompletion = { [weak self] in + self?.analytics.track(event: .closeAccountFailed(error: error)) DDLogError("⛔️ Cannot close account: \(error)") - self.presentErrorAlert(error: error) + self?.presentErrorAlert(error: error) } } + + self.dismissInProgressUI(completion: dismissInProgressUICompletion) } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift index 0bb75dd7bb3..b7f6191fd48 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift @@ -31,8 +31,8 @@ final class SettingsViewController: UIViewController { private lazy var removeAppleIDAccessCoordinator: RemoveAppleIDAccessCoordinator = RemoveAppleIDAccessCoordinator(sourceViewController: self) { [weak self] in - guard let self = self else { return .failure(RemoveAppleIDAccessError.presenterDeallocated) } - return await self.removeAppleIDAccess() + guard let self = self else { throw RemoveAppleIDAccessError.presenterDeallocated } + try await self.removeAppleIDAccess() } onRemoveSuccess: { [weak self] in self?.logOutUser() } @@ -284,11 +284,11 @@ private extension SettingsViewController { removeAppleIDAccessCoordinator.start() } - func removeAppleIDAccess() async -> Result { - await withCheckedContinuation { [weak self] continuation in + func removeAppleIDAccess() async throws { + try await withCheckedThrowingContinuation { [weak self] continuation in guard let self = self else { return } let action = AccountAction.closeAccount { result in - continuation.resume(returning: result) + continuation.resume(with: result) } self.stores.dispatch(action) } diff --git a/WooCommerce/WooCommerceTests/ViewModels/Authentication/AccountCreationFormViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewModels/Authentication/AccountCreationFormViewModelTests.swift index aecb3fd6358..807d72088b5 100644 --- a/WooCommerce/WooCommerceTests/ViewModels/Authentication/AccountCreationFormViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewModels/Authentication/AccountCreationFormViewModelTests.swift @@ -75,12 +75,16 @@ final class AccountCreationFormViewModelTests: XCTestCase { mockAccountCreationSuccess(result: .init(authToken: "token", username: "username")) XCTAssertFalse(stores.isAuthenticated) - // When - let result = await viewModel.createAccount() + do { + // When + try await viewModel.createAccount() - // Then - XCTAssertTrue(result.isSuccess) - XCTAssertTrue(stores.isAuthenticated) + // Then + XCTAssertTrue(stores.isAuthenticated) + + } catch { + XCTFail("Function should not throw an error") + } } func test_createAccount_password_failure_sets_passwordErrorMessage() async { @@ -89,13 +93,14 @@ final class AccountCreationFormViewModelTests: XCTestCase { XCTAssertFalse(stores.isAuthenticated) XCTAssertNil(viewModel.passwordErrorMessage) - // When - let result = await viewModel.createAccount() + do { + try await viewModel.createAccount() - // Then - XCTAssertTrue(result.isFailure) - XCTAssertFalse(stores.isAuthenticated) - XCTAssertEqual(viewModel.passwordErrorMessage, "too complex to guess") + XCTFail("Function should have thrown an error") + } catch { + XCTAssertFalse(stores.isAuthenticated) + XCTAssertEqual(viewModel.passwordErrorMessage, "too complex to guess") + } } func test_createAccount_invalidEmail_failure_sets_emailErrorMessage() async { @@ -103,12 +108,14 @@ final class AccountCreationFormViewModelTests: XCTestCase { mockAccountCreationFailure(error: .invalidEmail) XCTAssertNil(viewModel.emailErrorMessage) - // When - let result = await viewModel.createAccount() + do { + try await viewModel.createAccount() - // Then - XCTAssertTrue(result.isFailure) - XCTAssertNotNil(viewModel.emailErrorMessage) + XCTFail("Function should have thrown an error") + } catch { + // Then + XCTAssertNotNil(viewModel.emailErrorMessage) + } } func test_passwordErrorMessage_is_cleared_after_changing_password_input() async { @@ -116,7 +123,7 @@ final class AccountCreationFormViewModelTests: XCTestCase { mockAccountCreationFailure(error: .invalidPassword(message: "too complex to guess")) // When - let _ = await viewModel.createAccount() + try? await viewModel.createAccount() viewModel.password = "simple password" // Then @@ -130,7 +137,7 @@ final class AccountCreationFormViewModelTests: XCTestCase { mockAccountCreationFailure(error: .emailExists) // When - let _ = await viewModel.createAccount() + try? await viewModel.createAccount() viewModel.email = "real@woo.com" // Then @@ -146,7 +153,7 @@ final class AccountCreationFormViewModelTests: XCTestCase { mockAccountCreationSuccess(result: .init(authToken: "", username: "")) // When - let _ = await viewModel.createAccount() + try? await viewModel.createAccount() // Then XCTAssertEqual(analyticsProvider.receivedEvents, ["signup_submitted", "signup_success"]) @@ -157,7 +164,7 @@ final class AccountCreationFormViewModelTests: XCTestCase { mockAccountCreationFailure(error: .emailExists) // When - let _ = await viewModel.createAccount() + try? await viewModel.createAccount() // Then XCTAssertEqual(analyticsProvider.receivedEvents, ["signup_submitted", "signup_failed"]) diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Settings/RemoveAppleIDAccessCoordinatorTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Settings/RemoveAppleIDAccessCoordinatorTests.swift index 0c8644ea6ce..c6de161b7be 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Settings/RemoveAppleIDAccessCoordinatorTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Settings/RemoveAppleIDAccessCoordinatorTests.swift @@ -30,9 +30,7 @@ final class RemoveAppleIDAccessCoordinatorTests: XCTestCase { @MainActor func test_alert_is_presented_when_starting_coordinator() throws { // Given - let coordinator = RemoveAppleIDAccessCoordinator(sourceViewController: sourceViewController) { - return .success(()) - } onRemoveSuccess: {} + let coordinator = RemoveAppleIDAccessCoordinator(sourceViewController: sourceViewController) {} onRemoveSuccess: {} // When coordinator.start() diff --git a/WooFoundation/WooFoundation.xcodeproj/project.pbxproj b/WooFoundation/WooFoundation.xcodeproj/project.pbxproj index 2ecd09b8ac9..f2f1d59e792 100644 --- a/WooFoundation/WooFoundation.xcodeproj/project.pbxproj +++ b/WooFoundation/WooFoundation.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ 9FA5113235035AC9A6079B0D /* Pods_WooFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1733C61561AE3A1AD3C16B7 /* Pods_WooFoundation.framework */; }; AE948D0A28CF67CF009F3246 /* Date+StartAndEnd.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE948D0928CF67CE009F3246 /* Date+StartAndEnd.swift */; }; AE948D0D28CF6D50009F3246 /* DateStartAndEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE948D0C28CF6D50009F3246 /* DateStartAndEndTests.swift */; }; + B97190D1292CF3BC0065E413 /* Result+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97190D0292CF3BC0065E413 /* Result+Extensions.swift */; }; B987B06F284540D300C53CF6 /* CurrencyCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B987B06E284540D300C53CF6 /* CurrencyCode.swift */; }; B9C9C63F283E703C001B879F /* WooFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9C9C635283E703C001B879F /* WooFoundation.framework */; }; B9C9C659283E7195001B879F /* NSDecimalNumber+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C9C658283E7195001B879F /* NSDecimalNumber+Helpers.swift */; }; @@ -79,6 +80,7 @@ A21D73D352B4162AB096E276 /* Pods-WooFoundationTests.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WooFoundationTests.release-alpha.xcconfig"; path = "Target Support Files/Pods-WooFoundationTests/Pods-WooFoundationTests.release-alpha.xcconfig"; sourceTree = ""; }; AE948D0928CF67CE009F3246 /* Date+StartAndEnd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+StartAndEnd.swift"; sourceTree = ""; }; AE948D0C28CF6D50009F3246 /* DateStartAndEndTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateStartAndEndTests.swift; sourceTree = ""; }; + B97190D0292CF3BC0065E413 /* Result+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Extensions.swift"; sourceTree = ""; }; B987B06E284540D300C53CF6 /* CurrencyCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyCode.swift; sourceTree = ""; }; B9AED558283E7553002A2668 /* Yosemite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Yosemite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B9AED55B283E755A002A2668 /* Hardware.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Hardware.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -246,6 +248,7 @@ B9C9C658283E7195001B879F /* NSDecimalNumber+Helpers.swift */, 68FBC5B228926B2C00A05461 /* Collection+Extensions.swift */, 03B8C3882914083F002235B1 /* Bundle+Woo.swift */, + B97190D0292CF3BC0065E413 /* Result+Extensions.swift */, ); path = Extensions; sourceTree = ""; @@ -480,6 +483,7 @@ 03597A9B28F87BFC005E4A98 /* WooCommerceComUTMProvider.swift in Sources */, B9C9C663283E7296001B879F /* Logging.swift in Sources */, 6874E81428998AD300074A97 /* LogErrorAndExit.swift in Sources */, + B97190D1292CF3BC0065E413 /* Result+Extensions.swift in Sources */, B9C9C65D283E71C8001B879F /* CurrencyFormatter.swift in Sources */, AE948D0A28CF67CF009F3246 /* Date+StartAndEnd.swift in Sources */, 03597A9428F85686005E4A98 /* UTMParameters.swift in Sources */, diff --git a/WooFoundation/WooFoundation/Extensions/Result+Extensions.swift b/WooFoundation/WooFoundation/Extensions/Result+Extensions.swift new file mode 100644 index 00000000000..2b5f31c407f --- /dev/null +++ b/WooFoundation/WooFoundation/Extensions/Result+Extensions.swift @@ -0,0 +1,12 @@ +extension Result where Failure == Error { + /// Initializes asyncrhonously a `Result` with a `async throws` closure returning a object of the specified type + /// + public init(catching body: () async throws -> Success) async { + do { + let value = try await body() + self = .success(value) + } catch { + self = .failure(error) + } + } +} diff --git a/Yosemite/Yosemite/Stores/AccountCreationStore.swift b/Yosemite/Yosemite/Stores/AccountCreationStore.swift index 382546ed830..4873c810356 100644 --- a/Yosemite/Yosemite/Stores/AccountCreationStore.swift +++ b/Yosemite/Yosemite/Stores/AccountCreationStore.swift @@ -77,12 +77,7 @@ private extension AccountCreationStore { } func generateUsername(base: String) async -> String? { - let usernameSuggestionsResult = await remote.loadUsernameSuggestions(from: base) - guard case let .success(usernameSuggestions) = usernameSuggestionsResult, - let username = usernameSuggestions.first else { - return nil - } - return username + try? await remote.loadUsernameSuggestions(from: base).first } } diff --git a/Yosemite/Yosemite/Stores/DomainStore.swift b/Yosemite/Yosemite/Stores/DomainStore.swift index 41d80cb17bb..49378684805 100644 --- a/Yosemite/Yosemite/Stores/DomainStore.swift +++ b/Yosemite/Yosemite/Stores/DomainStore.swift @@ -1,5 +1,6 @@ import Foundation import Networking +import WooFoundation import protocol Storage.StorageManagerType /// Handles `DomainAction`. @@ -39,7 +40,7 @@ public final class DomainStore: Store { private extension DomainStore { func loadFreeDomainSuggestions(query: String, completion: @escaping (Result<[FreeDomainSuggestion], Error>) -> Void) { Task { @MainActor in - let result = await remote.loadFreeDomainSuggestions(query: query) + let result = await Result { try await remote.loadFreeDomainSuggestions(query: query) } completion(result) } } diff --git a/Yosemite/Yosemite/Stores/JustInTimeMessageStore.swift b/Yosemite/Yosemite/Stores/JustInTimeMessageStore.swift index 6ba9915b6cc..f16b9b4771d 100644 --- a/Yosemite/Yosemite/Stores/JustInTimeMessageStore.swift +++ b/Yosemite/Yosemite/Stores/JustInTimeMessageStore.swift @@ -1,6 +1,7 @@ import Foundation import Storage import Networking +import WooFoundation // MARK: - JustInTimeMessageStore // @@ -46,16 +47,20 @@ private extension JustInTimeMessageStore { hook: JustInTimeMessageHook, completion: @escaping (Result<[JustInTimeMessage], Error>) -> ()) { Task { - let result = await remote.loadAllJustInTimeMessages( - for: siteID, - messagePath: .init(app: .wooMobile, - screen: screen, - hook: hook), - query: justInTimeMessageQuery(), - locale: localeLanguageRegionIdentifier()) - let displayResult = result.map(displayMessages(_:)) + let result = await Result { + let messages = try await remote.loadAllJustInTimeMessages( + for: siteID, + messagePath: .init(app: .wooMobile, + screen: screen, + hook: hook), + query: justInTimeMessageQuery(), + locale: localeLanguageRegionIdentifier()) + + return displayMessages(messages) + } + await MainActor.run { - completion(displayResult) + completion(result) } } } @@ -114,9 +119,12 @@ private extension JustInTimeMessageStore { for siteID: Int64, completion: @escaping (Result) -> ()) { Task { - let result = await remote.dismissJustInTimeMessage(for: siteID, - messageID: message.messageID, - featureClass: message.featureClass) + let result = await Result { + try await remote.dismissJustInTimeMessage(for: siteID, + messageID: message.messageID, + featureClass: message.featureClass) + } + await MainActor.run { completion(result) } diff --git a/Yosemite/Yosemite/Stores/SiteStore.swift b/Yosemite/Yosemite/Stores/SiteStore.swift index b2be5532a85..c6f65511181 100644 --- a/Yosemite/Yosemite/Stores/SiteStore.swift +++ b/Yosemite/Yosemite/Stores/SiteStore.swift @@ -51,10 +51,9 @@ private extension SiteStore { domain: String, completion: @escaping (Result) -> Void) { Task { @MainActor in - let result = await remote.createSite(name: name, - domain: domain) - switch result { - case .success(let response): + do { + let response = try await remote.createSite(name: name, domain: domain) + guard response.success else { return completion(.failure(SiteCreationError.unsuccessful)) } @@ -65,8 +64,8 @@ private extension SiteStore { name: response.site.name, url: response.site.url, siteSlug: response.site.siteSlug))) - case .failure(let remoteError): - completion(.failure(SiteCreationError(remoteError: remoteError))) + } catch { + completion(.failure(SiteCreationError(remoteError: error))) } } } diff --git a/Yosemite/Yosemite/Stores/StatsStoreV4.swift b/Yosemite/Yosemite/Stores/StatsStoreV4.swift index cc5308eba9c..8c6144b42c8 100644 --- a/Yosemite/Yosemite/Stores/StatsStoreV4.swift +++ b/Yosemite/Yosemite/Stores/StatsStoreV4.swift @@ -1,7 +1,7 @@ import Foundation import Networking import Storage - +import WooFoundation // MARK: - StatsStoreV4 // @@ -155,26 +155,28 @@ private extension StatsStoreV4 { forceRefresh: Bool, onCompletion: @escaping (Result) -> Void) { Task { @MainActor in - let result = await loadTopEarnerStats(siteID: siteID, - timeRange: timeRange, - earliestDateToInclude: earliestDateToInclude, - latestDateToInclude: latestDateToInclude, - quantity: quantity, - forceRefresh: forceRefresh) - switch result { - case .success: - onCompletion(result) - case .failure(let error): + do { + try await loadTopEarnerStats(siteID: siteID, + timeRange: timeRange, + earliestDateToInclude: earliestDateToInclude, + latestDateToInclude: latestDateToInclude, + quantity: quantity, + forceRefresh: forceRefresh) + onCompletion(.success(())) + } catch { if let error = error as? DotcomError, error == .noRestRoute { - let resultFromDeprecatedAPI = await loadTopEarnerStatsWithDeprecatedAPI(siteID: siteID, - timeRange: timeRange, - earliestDateToInclude: earliestDateToInclude, - latestDateToInclude: latestDateToInclude, - quantity: quantity, - forceRefresh: forceRefresh) - onCompletion(resultFromDeprecatedAPI) - } else { + let result = await Result { + try await loadTopEarnerStatsWithDeprecatedAPI(siteID: siteID, + timeRange: timeRange, + earliestDateToInclude: earliestDateToInclude, + latestDateToInclude: latestDateToInclude, + quantity: quantity, + forceRefresh: forceRefresh) + } + onCompletion(result) + } else { + onCompletion(.failure(error)) } } } @@ -186,8 +188,8 @@ private extension StatsStoreV4 { earliestDateToInclude: Date, latestDateToInclude: Date, quantity: Int, - forceRefresh: Bool) async -> Result { - await withCheckedContinuation { continuation in + forceRefresh: Bool) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) -> Void in let dateFormatter = DateFormatter.Defaults.iso8601WithoutTimeZone let earliestDate = dateFormatter.string(from: earliestDateToInclude) let latestDate = dateFormatter.string(from: latestDateToInclude) @@ -208,10 +210,14 @@ private extension StatsStoreV4 { date: latestDateToInclude, leaderboards: leaderboards, quantity: quantity) { result in - continuation.resume(returning: result) + if case let .failure(error) = result { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: (())) + } } case .failure(let error): - continuation.resume(returning: .failure(error)) + continuation.resume(throwing: error) } } } @@ -223,8 +229,8 @@ private extension StatsStoreV4 { earliestDateToInclude: Date, latestDateToInclude: Date, quantity: Int, - forceRefresh: Bool) async -> Result { - await withCheckedContinuation { continuation in + forceRefresh: Bool) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) -> Void in let dateFormatter = DateFormatter.Defaults.iso8601WithoutTimeZone let earliestDate = dateFormatter.string(from: earliestDateToInclude) let latestDate = dateFormatter.string(from: latestDateToInclude) @@ -245,10 +251,14 @@ private extension StatsStoreV4 { date: latestDateToInclude, leaderboards: leaderboards, quantity: quantity) { result in - continuation.resume(returning: result) + if case let .failure(error) = result { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: ()) + } } case .failure(let error): - continuation.resume(returning: .failure(error)) + continuation.resume(throwing: error) } } } diff --git a/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockAccountRemote.swift b/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockAccountRemote.swift index 4f56dfb110d..5accf9da12c 100644 --- a/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockAccountRemote.swift +++ b/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockAccountRemote.swift @@ -98,12 +98,13 @@ extension MockAccountRemote: AccountRemoteProtocol { // no-op } - func loadUsernameSuggestions(from text: String) async -> Result<[String], Error> { + func loadUsernameSuggestions(from text: String) async throws -> [String] { guard let result = loadUsernameSuggestionsResult else { XCTFail("Could not find result for loading username suggestions.") - return .failure(NetworkError.notFound) + throw NetworkError.notFound } - return result + + return try result.get() } func createAccount(email: String, diff --git a/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockDomainRemote.swift b/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockDomainRemote.swift index 820ef55db59..d62891b695c 100644 --- a/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockDomainRemote.swift +++ b/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockDomainRemote.swift @@ -14,11 +14,11 @@ final class MockDomainRemote { } extension MockDomainRemote: DomainRemoteProtocol { - func loadFreeDomainSuggestions(query: String) async -> Result<[FreeDomainSuggestion], Error> { + func loadFreeDomainSuggestions(query: String) async throws -> [FreeDomainSuggestion] { guard let result = loadDomainSuggestionsResult else { XCTFail("Could not find result for loading domain suggestions.") - return .failure(NetworkError.notFound) + throw NetworkError.notFound } - return result + return try result.get() } } diff --git a/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift b/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift index 411777231a9..8db0deae7c5 100644 --- a/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift +++ b/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift @@ -14,11 +14,12 @@ final class MockSiteRemote { } extension MockSiteRemote: SiteRemoteProtocol { - func createSite(name: String, domain: String) async -> Result { + func createSite(name: String, domain: String) async throws -> SiteCreationResponse { guard let result = createSiteResult else { XCTFail("Could not find result for creating a site.") - return .failure(NetworkError.notFound) + throw NetworkError.notFound } - return result + + return try result.get() } }