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
8 changes: 8 additions & 0 deletions Networking/Networking.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
/* Begin PBXBuildFile section */
020220E223966CD900290165 /* product-shipping-classes-load-one.json in Resources */ = {isa = PBXBuildFile; fileRef = 020220E123966CD900290165 /* product-shipping-classes-load-one.json */; };
0205021C27C86B9700FB1C6B /* inbox-note-without-isRead.json in Resources */ = {isa = PBXBuildFile; fileRef = 0205021B27C86B9700FB1C6B /* inbox-note-without-isRead.json */; };
02050F57296FE90A00710E63 /* domain-suggestions-paid.json in Resources */ = {isa = PBXBuildFile; fileRef = 02050F56296FE90A00710E63 /* domain-suggestions-paid.json */; };
02050F59296FEC5B00710E63 /* domain-products.json in Resources */ = {isa = PBXBuildFile; fileRef = 02050F58296FEC5B00710E63 /* domain-products.json */; };
020C907B24C6E108001E2BEB /* product-variation-update.json in Resources */ = {isa = PBXBuildFile; fileRef = 020C907A24C6E108001E2BEB /* product-variation-update.json */; };
020C907F24C7D359001E2BEB /* ProductVariationMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020C907E24C7D359001E2BEB /* ProductVariationMapperTests.swift */; };
020D07B823D852BB00FD9580 /* Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020D07B723D852BB00FD9580 /* Media.swift */; };
Expand Down Expand Up @@ -826,6 +828,8 @@
/* Begin PBXFileReference section */
020220E123966CD900290165 /* product-shipping-classes-load-one.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-shipping-classes-load-one.json"; sourceTree = "<group>"; };
0205021B27C86B9700FB1C6B /* inbox-note-without-isRead.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "inbox-note-without-isRead.json"; sourceTree = "<group>"; };
02050F56296FE90A00710E63 /* domain-suggestions-paid.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "domain-suggestions-paid.json"; sourceTree = "<group>"; };
02050F58296FEC5B00710E63 /* domain-products.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "domain-products.json"; sourceTree = "<group>"; };
020C907A24C6E108001E2BEB /* product-variation-update.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-variation-update.json"; sourceTree = "<group>"; };
020C907E24C7D359001E2BEB /* ProductVariationMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductVariationMapperTests.swift; sourceTree = "<group>"; };
020D07B723D852BB00FD9580 /* Media.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Media.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2167,7 +2171,9 @@
028CB71A290224D700331C09 /* create-account-error-username.json */,
028CB712290223CB00331C09 /* create-account-success.json */,
02EF166F292F0CF400D90AD6 /* create-cart-success.json */,
02050F58296FEC5B00710E63 /* domain-products.json */,
0239306C291A973F00B2632F /* domain-suggestions.json */,
02050F56296FE90A00710E63 /* domain-suggestions-paid.json */,
DE50295F28C609A300551736 /* jetpack-connected-user.json */,
DE34051A28BDF12C00CF0D97 /* jetpack-connection-url.json */,
DE50296228C609DE00551736 /* jetpack-user-not-connected.json */,
Expand Down Expand Up @@ -3115,6 +3121,7 @@
02BA23CA22EEF62C009539E7 /* order-stats-v4-wcadmin-activated.json in Resources */,
31A451D627863A2E00FE81AA /* stripe-account-unknown-status.json in Resources */,
45152819257A84A60076B03C /* product-attributes-all.json in Resources */,
02050F59296FEC5B00710E63 /* domain-products.json in Resources */,
45AB8B1324AA34CB00B5B36E /* product-tags-deleted.json in Resources */,
02698CFC24C1B0CE005337C4 /* product-variations-load-all-first-on-sale-empty-sale-price.json in Resources */,
31A451D427863A2E00FE81AA /* stripe-account-rejected-listed.json in Resources */,
Expand All @@ -3137,6 +3144,7 @@
D8A284F425FBB48D0019A84B /* product-attribute-terms.json in Resources */,
74E30951216E8DCE00ABCE4C /* site-visits-alt.json in Resources */,
74ABA1C5213F17AA00FFAD30 /* top-performers-day.json in Resources */,
02050F57296FE90A00710E63 /* domain-suggestions-paid.json in Resources */,
31B8D6B626583970008E3DB2 /* wcpay-account-implicitly-not-eligible.json in Resources */,
26B2F74724C55A6E0065CCC8 /* leaderboards-year.json in Resources */,
31054714262E2F3B00C5C02B /* wcpay-payment-intent-requires-payment-method.json in Resources */,
Expand Down
67 changes: 67 additions & 0 deletions Networking/Networking/Remote/DomainRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ public protocol DomainRemoteProtocol {
/// - Returns: The result of free domain suggestions.
func loadFreeDomainSuggestions(query: String) async throws -> [FreeDomainSuggestion]

/// Loads domain suggestions that are not free based on the query.
/// - Parameter query: What the domain suggestions are based on.
/// - Returns: A list of paid domain suggestions.
func loadPaidDomainSuggestions(query: String) async throws -> [PaidDomainSuggestion]

/// Loads WPCOM domain products for domain cost and sale info in `loadPaidDomainSuggestions`.
/// - Returns: A list of domain products.
func loadDomainProducts() async throws -> [DomainProduct]

/// Loads all domains for a site.
/// - Parameter siteID: ID of the site to load the domains for.
/// - Returns: A list of domains.
Expand All @@ -27,6 +36,26 @@ public class DomainRemote: Remote, DomainRemoteProtocol {
return try await enqueue(request)
}

public func loadPaidDomainSuggestions(query: String) async throws -> [PaidDomainSuggestion] {
let path = Path.domainSuggestions
let parameters: [String: Any] = [
ParameterKey.query: query,
ParameterKey.quantity: Defaults.domainSuggestionsQuantity
]
let request = DotcomRequest(wordpressApiVersion: .mark1_1, method: .get, path: path, parameters: parameters)
return try await enqueue(request)
}

public func loadDomainProducts() async throws -> [DomainProduct] {
let path = Path.domainProducts
let parameters: [String: Any] = [
ParameterKey.domainProductType: "domains"
]
let request = DotcomRequest(wordpressApiVersion: .mark1_1, method: .get, path: path, parameters: parameters)
let productsByName: [String: DomainProduct] = try await enqueue(request)
return Array(productsByName.values)
}

public func loadDomains(siteID: Int64) async throws -> [SiteDomain] {
let path = "sites/\(siteID)/\(Path.domains)"
let request = DotcomRequest(wordpressApiVersion: .mark1_1, method: .get, path: path)
Expand All @@ -53,6 +82,41 @@ public struct FreeDomainSuggestion: Decodable, Equatable {
}
}

/// Necessary data for a paid domain suggestion.
public struct PaidDomainSuggestion: Decodable, Equatable {
/// Domain name.
public let name: String
/// WPCOM product ID.
public let productID: Int64
/// Whether there is privacy support. Used when creating a cart with a domain product.
public let supportsPrivacy: Bool

private enum CodingKeys: String, CodingKey {
case name = "domain_name"
case productID = "product_id"
case supportsPrivacy = "supports_privacy"
}
}

/// Necessary data for a WPCOM domain product.
public struct DomainProduct: Decodable, Equatable {
/// WPCOM product ID.
public let productID: Int64
/// The duration of the product, localized on the backend (e.g. "year").
public let term: String
/// Cost string including the currency.
public let cost: String
/// Optional sale cost string including the currency.
public let saleCost: String?

private enum CodingKeys: String, CodingKey {
case productID = "product_id"
case term = "product_term"
case cost = "combined_cost_display"
case saleCost = "combined_sale_cost_display"
}
}

/// Necessary data for a site's domain.
public struct SiteDomain: Decodable, Equatable {
/// Domain name.
Expand Down Expand Up @@ -114,10 +178,13 @@ private extension DomainRemote {
static let quantity = "quantity"
/// Whether to restrict suggestions to only wordpress.com subdomains. If `true`, only `quantity` and `query` parameters are respected.
static let wordPressDotComSubdomainsOnly = "only_wordpressdotcom"
/// The type of WPCOM products.
static let domainProductType = "type"
}

enum Path {
static let domainSuggestions = "domains/suggestions"
static let domainProducts = "products"
static let domains = "domains"
}
}
50 changes: 50 additions & 0 deletions Networking/NetworkingTests/Remote/DomainRemoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,56 @@ final class DomainRemoteTests: XCTestCase {
await assertThrowsError({_ = try await remote.loadFreeDomainSuggestions(query: "domain")}, errorAssert: { ($0 as? NetworkError) == .notFound })
}

// MARK: - `loadPaidDomainSuggestions`

func test_loadPaidDomainSuggestions_returns_suggestions_on_success() async throws {
// Given
let remote = DomainRemote(network: network)
network.simulateResponse(requestUrlSuffix: "domains/suggestions", filename: "domain-suggestions-paid")

// When
let suggestions = try await remote.loadPaidDomainSuggestions(query: "domain")

// Then
XCTAssertEqual(suggestions, [
.init(name: "color.bar", productID: 356, supportsPrivacy: true),
.init(name: "color.ink", productID: 359, supportsPrivacy: true)
])
}

func test_loadPaidDomainSuggestions_returns_error_on_empty_response() async throws {
// Given
let remote = DomainRemote(network: network)

await assertThrowsError({_ = try await remote.loadPaidDomainSuggestions(query: "domain")}, errorAssert: { ($0 as? NetworkError) == .notFound })
}

// MARK: - `loadDomainProducts`

func test_loadDomainProducts_returns_products_on_success() async throws {
// Given
let remote = DomainRemote(network: network)
network.simulateResponse(requestUrlSuffix: "products", filename: "domain-products")

// When
// Products are in random order because of the product name mapping.
// They are sorted here to ensure the same order for unit testing.
let products = try await remote.loadDomainProducts().sorted(by: { $0.productID < $1.productID })

// Then
XCTAssertEqual(products, [
.init(productID: 355, term: "year", cost: "US$15.00", saleCost: "US$3.90"),
.init(productID: 356, term: "year", cost: "US$60.00", saleCost: nil)
])
}

func test_loadDomainProducts_returns_error_on_empty_response() async throws {
// Given
let remote = DomainRemote(network: network)

await assertThrowsError({_ = try await remote.loadDomainProducts()}, errorAssert: { ($0 as? NetworkError) == .notFound })
}

// MARK: - `loadDomains`

func test_loadDomains_returns_domains_on_success() async throws {
Expand Down
64 changes: 64 additions & 0 deletions Networking/NetworkingTests/Responses/domain-products.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"dotart_domain": {
"product_id": 355,
"product_name": ".art Domain Registration",
"product_slug": "dotart_domain",
"description": "",
"product_type": "domain_reg",
"available": true,
"billing_product_slug": "wp-dot-art-registration",
"is_domain_registration": true,
"cost_display": "US$2.00",
"combined_cost_display": "US$15.00",
"cost": 2,
"cost_smallest_unit": 200,
"currency_code": "USD",
"price_tier_list": [],
"price_tier_usage_quantity": null,
"product_term": "year",
"price_tiers": [],
"price_tier_slug": "",
"tld": "art",
"is_privacy_protection_product_purchase_allowed": true,
"sale_cost": 3.9,
"combined_sale_cost_display": "US$3.90",
"sale_coupon": {
"start_date": "2022-12-01 00:01:00",
"expires": "2023-03-31 00:00:00",
"discount": 74,
"purchase_types": [
3
],
"product_ids": [
355
],
"allowed_for_domain_transfers": false,
"allowed_for_renewals": false,
"allowed_for_new_purchases": true,
"code": "7ea7af1f76879264",
"tld_rank": null
}
},
"dotbar_domain": {
"product_id": 356,
"product_name": ".bar Domain Registration",
"product_slug": "dotbar_domain",
"description": "",
"product_type": "domain_reg",
"available": true,
"billing_product_slug": "wp-dot-bar-registration",
"is_domain_registration": true,
"cost_display": "US$47.00",
"combined_cost_display": "US$60.00",
"cost": 47,
"cost_smallest_unit": 4700,
"currency_code": "USD",
"price_tier_list": [],
"price_tier_usage_quantity": null,
"product_term": "year",
"price_tiers": [],
"price_tier_slug": "",
"tld": "bar",
"is_privacy_protection_product_purchase_allowed": true
}
}
30 changes: 30 additions & 0 deletions Networking/NetworkingTests/Responses/domain-suggestions-paid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[
{
"domain_name": "color.bar",
"relevance": 0.33,
"supports_privacy": true,
"vendor": "donuts",
"match_reasons": [
"exact-match"
],
"product_id": 356,
"product_slug": "dotbar_domain",
"cost": "US$60.00",
"raw_price": 60,
"currency_code": "USD"
},
{
"domain_name": "color.ink",
"relevance": 0.33,
"supports_privacy": true,
"vendor": "donuts",
"match_reasons": [
"exact-match"
],
"product_id": 359,
"product_slug": "dotink_domain",
"cost": "US$25.00",
"raw_price": 25,
"currency_code": "USD"
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ extension MockDomainRemote: DomainRemoteProtocol {
return try result.get()
}

func loadPaidDomainSuggestions(query: String) async throws -> [PaidDomainSuggestion] {
// TODO: 8558 - Yosemite layer for paid domains
throw NetworkError.notFound
}

func loadDomainProducts() async throws -> [DomainProduct] {
// TODO: 8558 - Yosemite layer for paid domains
throw NetworkError.notFound
}

func loadDomains(siteID: Int64) async throws -> [SiteDomain] {
guard let result = loadDomainsResult else {
XCTFail("Could not find result for loading domains.")
Expand Down