diff --git a/Yosemite/Yosemite/Actions/DomainAction.swift b/Yosemite/Yosemite/Actions/DomainAction.swift index 09526aa86d2..3bd77b0e9c4 100644 --- a/Yosemite/Yosemite/Actions/DomainAction.swift +++ b/Yosemite/Yosemite/Actions/DomainAction.swift @@ -4,5 +4,20 @@ import Foundation // public enum DomainAction: Action { case loadFreeDomainSuggestions(query: String, completion: (Result<[FreeDomainSuggestion], Error>) -> Void) + case loadPaidDomainSuggestions(query: String, completion: (Result<[PaidDomainSuggestion], Error>) -> Void) case loadDomains(siteID: Int64, completion: (Result<[SiteDomain], Error>) -> Void) } + +/// Necessary data for the domain selector flow with paid domains. +public struct PaidDomainSuggestion: Equatable { + /// ID of the WPCOM product. + public let productID: Int64 + /// Domain name. + public let name: String + /// Duration of the product subscription (e.g. "year"), localized on the backend. + public let term: String + /// Cost string including the currency. + public let cost: String + /// Optional sale cost string including the currency. + public let saleCost: String? +} diff --git a/Yosemite/Yosemite/Stores/DomainStore.swift b/Yosemite/Yosemite/Stores/DomainStore.swift index 166786ca846..78ae49fd217 100644 --- a/Yosemite/Yosemite/Stores/DomainStore.swift +++ b/Yosemite/Yosemite/Stores/DomainStore.swift @@ -33,6 +33,8 @@ public final class DomainStore: Store { switch action { case .loadFreeDomainSuggestions(let query, let completion): loadFreeDomainSuggestions(query: query, completion: completion) + case .loadPaidDomainSuggestions(let query, let completion): + loadPaidDomainSuggestions(query: query, completion: completion) case .loadDomains(let siteID, let completion): loadDomains(siteID: siteID, completion: completion) } @@ -47,6 +49,35 @@ private extension DomainStore { } } + func loadPaidDomainSuggestions(query: String, completion: @escaping (Result<[PaidDomainSuggestion], Error>) -> Void) { + Task { @MainActor in + do { + // Fetches domain products and domain suggestions at the same time. + async let domainProducts = remote.loadDomainProducts() + async let domainSuggestions = remote.loadPaidDomainSuggestions(query: query) + let domainProductsByID = try await domainProducts.reduce([Int64: DomainProduct](), { partialResult, product in + var productsByID = partialResult + productsByID[product.productID] = product + return productsByID + }) + let paidDomainSuggestions: [PaidDomainSuggestion] = try await domainSuggestions.compactMap { domainSuggestion in + let productID = domainSuggestion.productID + guard let domainProduct = domainProductsByID[productID] else { + return nil + } + return PaidDomainSuggestion(productID: domainSuggestion.productID, + name: domainSuggestion.name, + term: domainProduct.term, + cost: domainProduct.cost, + saleCost: domainProduct.saleCost) + } + completion(.success(paidDomainSuggestions)) + } catch { + completion(.failure(error)) + } + } + } + func loadDomains(siteID: Int64, completion: @escaping (Result<[SiteDomain], Error>) -> Void) { Task { @MainActor in let result = await Result { try await remote.loadDomains(siteID: siteID) } diff --git a/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockDomainRemote.swift b/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockDomainRemote.swift index 74bdfc074db..ce91a483ee2 100644 --- a/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockDomainRemote.swift +++ b/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockDomainRemote.swift @@ -4,9 +4,15 @@ import XCTest /// Mock for `DomainRemote`. /// final class MockDomainRemote { - /// The results to return in `loadDomainSuggestions`. + /// The results to return in `loadFreeDomainSuggestions`. private var loadDomainSuggestionsResult: Result<[FreeDomainSuggestion], Error>? + /// The results to return in `loadPaidDomainSuggestions`. + private var loadPaidDomainSuggestionsResult: Result<[PaidDomainSuggestion], Error>? + + /// The results to return in `loadDomainProducts`. + private var loadDomainProductsResult: Result<[DomainProduct], Error>? + /// The results to return in `loadDomains`. private var loadDomainsResult: Result<[SiteDomain], Error>? @@ -15,6 +21,16 @@ final class MockDomainRemote { loadDomainSuggestionsResult = result } + /// Returns the value when `loadPaidDomainSuggestions` is called. + func whenLoadingPaidDomainSuggestions(thenReturn result: Result<[PaidDomainSuggestion], Error>) { + loadPaidDomainSuggestionsResult = result + } + + /// Returns the value when `loadDomainProducts` is called. + func whenLoadingDomainProducts(thenReturn result: Result<[DomainProduct], Error>) { + loadDomainProductsResult = result + } + /// Returns the value when `loadDomains` is called. func whenLoadingDomains(thenReturn result: Result<[SiteDomain], Error>) { loadDomainsResult = result @@ -31,13 +47,19 @@ extension MockDomainRemote: DomainRemoteProtocol { } func loadPaidDomainSuggestions(query: String) async throws -> [PaidDomainSuggestion] { - // TODO: 8558 - Yosemite layer for paid domains - throw NetworkError.notFound + guard let result = loadPaidDomainSuggestionsResult else { + XCTFail("Could not find result for loading domain suggestions.") + throw NetworkError.notFound + } + return try result.get() } func loadDomainProducts() async throws -> [DomainProduct] { - // TODO: 8558 - Yosemite layer for paid domains - throw NetworkError.notFound + guard let result = loadDomainProductsResult else { + XCTFail("Could not find result for loading domain products.") + throw NetworkError.notFound + } + return try result.get() } func loadDomains(siteID: Int64) async throws -> [SiteDomain] { diff --git a/Yosemite/YosemiteTests/Stores/DomainStoreTests.swift b/Yosemite/YosemiteTests/Stores/DomainStoreTests.swift index 14e7584fca6..b6a97d55255 100644 --- a/Yosemite/YosemiteTests/Stores/DomainStoreTests.swift +++ b/Yosemite/YosemiteTests/Stores/DomainStoreTests.swift @@ -71,6 +71,65 @@ final class DomainStoreTests: XCTestCase { XCTAssertEqual(error as? NetworkError, .timeout) } + // MARK: - `loadPaidDomainSuggestions` + + func test_loadPaidDomainSuggestions_returns_suggestions_on_success() throws { + // Given + remote.whenLoadingPaidDomainSuggestions(thenReturn: .success([.init(name: "paid.domain", productID: 203, supportsPrivacy: true)])) + remote.whenLoadingDomainProducts(thenReturn: .success([.init(productID: 203, term: "year", cost: "NT$610.00", saleCost: "NT$154.00")])) + + // When + let result = waitFor { promise in + self.store.onAction(DomainAction.loadPaidDomainSuggestions(query: "domain") { result in + promise(result) + }) + } + + // Then + XCTAssertTrue(result.isSuccess) + let suggestions = try XCTUnwrap(result.get()) + XCTAssertEqual(suggestions, [.init(productID: 203, name: "paid.domain", term: "year", cost: "NT$610.00", saleCost: "NT$154.00")]) + } + + func test_loadPaidDomainSuggestions_returns_empty_suggestions_from_failed_productID_mapping() throws { + // Given + remote.whenLoadingPaidDomainSuggestions(thenReturn: .success([.init(name: "paid.domain", productID: 203, supportsPrivacy: true)])) + // The product ID does not match the domain suggestion. + remote.whenLoadingDomainProducts(thenReturn: .success([.init(productID: 156, term: "year", cost: "NT$610.00", saleCost: "NT$154.00")])) + + // When + let result = waitFor { promise in + self.store.onAction(DomainAction.loadPaidDomainSuggestions(query: "domain") { result in + promise(result) + }) + } + + // Then + XCTAssertTrue(result.isSuccess) + let suggestions = try XCTUnwrap(result.get()) + XCTAssertEqual(suggestions, []) + } + + func test_loadPaidDomainSuggestions_returns_error_on_failure() throws { + // Given + remote.whenLoadingPaidDomainSuggestions(thenReturn: .failure(NetworkError.invalidURL)) + remote.whenLoadingDomainProducts(thenReturn: .failure(NetworkError.timeout)) + + // When + let result = waitFor { promise in + self.store.onAction(DomainAction.loadPaidDomainSuggestions(query: "domain") { result in + promise(result) + }) + } + + // Then + XCTAssertTrue(result.isFailure) + let error = try XCTUnwrap(result.failure) + // The error of `loadDomainProducts` is returned since it is the first async call. + XCTAssertEqual(error as? NetworkError, .timeout) + } + + // MARK: - `loadDomains` func test_loadDomains_returns_domains_on_success() throws {