Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Yosemite/Yosemite/Actions/DomainAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
}
31 changes: 31 additions & 0 deletions Yosemite/Yosemite/Stores/DomainStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>?

Expand All @@ -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
Expand All @@ -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] {
Expand Down
59 changes: 59 additions & 0 deletions Yosemite/YosemiteTests/Stores/DomainStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down