Skip to content

Commit f6b5a88

Browse files
authored
Merge pull request #8642 from woocommerce/feat/8558-paid-domain-search-networking
Domain selector for paid domains: Networking layer changes
2 parents 898b82d + 6a68004 commit f6b5a88

File tree

6 files changed

+229
-0
lines changed

6 files changed

+229
-0
lines changed

Networking/Networking.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
/* Begin PBXBuildFile section */
1010
020220E223966CD900290165 /* product-shipping-classes-load-one.json in Resources */ = {isa = PBXBuildFile; fileRef = 020220E123966CD900290165 /* product-shipping-classes-load-one.json */; };
1111
0205021C27C86B9700FB1C6B /* inbox-note-without-isRead.json in Resources */ = {isa = PBXBuildFile; fileRef = 0205021B27C86B9700FB1C6B /* inbox-note-without-isRead.json */; };
12+
02050F57296FE90A00710E63 /* domain-suggestions-paid.json in Resources */ = {isa = PBXBuildFile; fileRef = 02050F56296FE90A00710E63 /* domain-suggestions-paid.json */; };
13+
02050F59296FEC5B00710E63 /* domain-products.json in Resources */ = {isa = PBXBuildFile; fileRef = 02050F58296FEC5B00710E63 /* domain-products.json */; };
1214
020C907B24C6E108001E2BEB /* product-variation-update.json in Resources */ = {isa = PBXBuildFile; fileRef = 020C907A24C6E108001E2BEB /* product-variation-update.json */; };
1315
020C907F24C7D359001E2BEB /* ProductVariationMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020C907E24C7D359001E2BEB /* ProductVariationMapperTests.swift */; };
1416
020D07B823D852BB00FD9580 /* Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020D07B723D852BB00FD9580 /* Media.swift */; };
@@ -832,6 +834,8 @@
832834
/* Begin PBXFileReference section */
833835
020220E123966CD900290165 /* product-shipping-classes-load-one.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-shipping-classes-load-one.json"; sourceTree = "<group>"; };
834836
0205021B27C86B9700FB1C6B /* inbox-note-without-isRead.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "inbox-note-without-isRead.json"; sourceTree = "<group>"; };
837+
02050F56296FE90A00710E63 /* domain-suggestions-paid.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "domain-suggestions-paid.json"; sourceTree = "<group>"; };
838+
02050F58296FEC5B00710E63 /* domain-products.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "domain-products.json"; sourceTree = "<group>"; };
835839
020C907A24C6E108001E2BEB /* product-variation-update.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-variation-update.json"; sourceTree = "<group>"; };
836840
020C907E24C7D359001E2BEB /* ProductVariationMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductVariationMapperTests.swift; sourceTree = "<group>"; };
837841
020D07B723D852BB00FD9580 /* Media.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Media.swift; sourceTree = "<group>"; };
@@ -2185,7 +2189,9 @@
21852189
028CB71A290224D700331C09 /* create-account-error-username.json */,
21862190
028CB712290223CB00331C09 /* create-account-success.json */,
21872191
02EF166F292F0CF400D90AD6 /* create-cart-success.json */,
2192+
02050F58296FEC5B00710E63 /* domain-products.json */,
21882193
0239306C291A973F00B2632F /* domain-suggestions.json */,
2194+
02050F56296FE90A00710E63 /* domain-suggestions-paid.json */,
21892195
DE50295F28C609A300551736 /* jetpack-connected-user.json */,
21902196
DE34051A28BDF12C00CF0D97 /* jetpack-connection-url.json */,
21912197
DE50296228C609DE00551736 /* jetpack-user-not-connected.json */,
@@ -3136,6 +3142,7 @@
31363142
02BA23CA22EEF62C009539E7 /* order-stats-v4-wcadmin-activated.json in Resources */,
31373143
31A451D627863A2E00FE81AA /* stripe-account-unknown-status.json in Resources */,
31383144
45152819257A84A60076B03C /* product-attributes-all.json in Resources */,
3145+
02050F59296FEC5B00710E63 /* domain-products.json in Resources */,
31393146
45AB8B1324AA34CB00B5B36E /* product-tags-deleted.json in Resources */,
31403147
02698CFC24C1B0CE005337C4 /* product-variations-load-all-first-on-sale-empty-sale-price.json in Resources */,
31413148
31A451D427863A2E00FE81AA /* stripe-account-rejected-listed.json in Resources */,
@@ -3158,6 +3165,7 @@
31583165
D8A284F425FBB48D0019A84B /* product-attribute-terms.json in Resources */,
31593166
74E30951216E8DCE00ABCE4C /* site-visits-alt.json in Resources */,
31603167
74ABA1C5213F17AA00FFAD30 /* top-performers-day.json in Resources */,
3168+
02050F57296FE90A00710E63 /* domain-suggestions-paid.json in Resources */,
31613169
31B8D6B626583970008E3DB2 /* wcpay-account-implicitly-not-eligible.json in Resources */,
31623170
26B2F74724C55A6E0065CCC8 /* leaderboards-year.json in Resources */,
31633171
31054714262E2F3B00C5C02B /* wcpay-payment-intent-requires-payment-method.json in Resources */,

Networking/Networking/Remote/DomainRemote.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ public protocol DomainRemoteProtocol {
77
/// - Returns: The result of free domain suggestions.
88
func loadFreeDomainSuggestions(query: String) async throws -> [FreeDomainSuggestion]
99

10+
/// Loads domain suggestions that are not free based on the query.
11+
/// - Parameter query: What the domain suggestions are based on.
12+
/// - Returns: A list of paid domain suggestions.
13+
func loadPaidDomainSuggestions(query: String) async throws -> [PaidDomainSuggestion]
14+
15+
/// Loads WPCOM domain products for domain cost and sale info in `loadPaidDomainSuggestions`.
16+
/// - Returns: A list of domain products.
17+
func loadDomainProducts() async throws -> [DomainProduct]
18+
1019
/// Loads all domains for a site.
1120
/// - Parameter siteID: ID of the site to load the domains for.
1221
/// - Returns: A list of domains.
@@ -27,6 +36,26 @@ public class DomainRemote: Remote, DomainRemoteProtocol {
2736
return try await enqueue(request)
2837
}
2938

39+
public func loadPaidDomainSuggestions(query: String) async throws -> [PaidDomainSuggestion] {
40+
let path = Path.domainSuggestions
41+
let parameters: [String: Any] = [
42+
ParameterKey.query: query,
43+
ParameterKey.quantity: Defaults.domainSuggestionsQuantity
44+
]
45+
let request = DotcomRequest(wordpressApiVersion: .mark1_1, method: .get, path: path, parameters: parameters)
46+
return try await enqueue(request)
47+
}
48+
49+
public func loadDomainProducts() async throws -> [DomainProduct] {
50+
let path = Path.domainProducts
51+
let parameters: [String: Any] = [
52+
ParameterKey.domainProductType: "domains"
53+
]
54+
let request = DotcomRequest(wordpressApiVersion: .mark1_1, method: .get, path: path, parameters: parameters)
55+
let productsByName: [String: DomainProduct] = try await enqueue(request)
56+
return Array(productsByName.values)
57+
}
58+
3059
public func loadDomains(siteID: Int64) async throws -> [SiteDomain] {
3160
let path = "sites/\(siteID)/\(Path.domains)"
3261
let request = DotcomRequest(wordpressApiVersion: .mark1_1, method: .get, path: path)
@@ -53,6 +82,41 @@ public struct FreeDomainSuggestion: Decodable, Equatable {
5382
}
5483
}
5584

85+
/// Necessary data for a paid domain suggestion.
86+
public struct PaidDomainSuggestion: Decodable, Equatable {
87+
/// Domain name.
88+
public let name: String
89+
/// WPCOM product ID.
90+
public let productID: Int64
91+
/// Whether there is privacy support. Used when creating a cart with a domain product.
92+
public let supportsPrivacy: Bool
93+
94+
private enum CodingKeys: String, CodingKey {
95+
case name = "domain_name"
96+
case productID = "product_id"
97+
case supportsPrivacy = "supports_privacy"
98+
}
99+
}
100+
101+
/// Necessary data for a WPCOM domain product.
102+
public struct DomainProduct: Decodable, Equatable {
103+
/// WPCOM product ID.
104+
public let productID: Int64
105+
/// The duration of the product, localized on the backend (e.g. "year").
106+
public let term: String
107+
/// Cost string including the currency.
108+
public let cost: String
109+
/// Optional sale cost string including the currency.
110+
public let saleCost: String?
111+
112+
private enum CodingKeys: String, CodingKey {
113+
case productID = "product_id"
114+
case term = "product_term"
115+
case cost = "combined_cost_display"
116+
case saleCost = "combined_sale_cost_display"
117+
}
118+
}
119+
56120
/// Necessary data for a site's domain.
57121
public struct SiteDomain: Decodable, Equatable {
58122
/// Domain name.
@@ -114,10 +178,13 @@ private extension DomainRemote {
114178
static let quantity = "quantity"
115179
/// Whether to restrict suggestions to only wordpress.com subdomains. If `true`, only `quantity` and `query` parameters are respected.
116180
static let wordPressDotComSubdomainsOnly = "only_wordpressdotcom"
181+
/// The type of WPCOM products.
182+
static let domainProductType = "type"
117183
}
118184

119185
enum Path {
120186
static let domainSuggestions = "domains/suggestions"
187+
static let domainProducts = "products"
121188
static let domains = "domains"
122189
}
123190
}

Networking/NetworkingTests/Remote/DomainRemoteTests.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,56 @@ final class DomainRemoteTests: XCTestCase {
4040
await assertThrowsError({_ = try await remote.loadFreeDomainSuggestions(query: "domain")}, errorAssert: { ($0 as? NetworkError) == .notFound })
4141
}
4242

43+
// MARK: - `loadPaidDomainSuggestions`
44+
45+
func test_loadPaidDomainSuggestions_returns_suggestions_on_success() async throws {
46+
// Given
47+
let remote = DomainRemote(network: network)
48+
network.simulateResponse(requestUrlSuffix: "domains/suggestions", filename: "domain-suggestions-paid")
49+
50+
// When
51+
let suggestions = try await remote.loadPaidDomainSuggestions(query: "domain")
52+
53+
// Then
54+
XCTAssertEqual(suggestions, [
55+
.init(name: "color.bar", productID: 356, supportsPrivacy: true),
56+
.init(name: "color.ink", productID: 359, supportsPrivacy: true)
57+
])
58+
}
59+
60+
func test_loadPaidDomainSuggestions_returns_error_on_empty_response() async throws {
61+
// Given
62+
let remote = DomainRemote(network: network)
63+
64+
await assertThrowsError({_ = try await remote.loadPaidDomainSuggestions(query: "domain")}, errorAssert: { ($0 as? NetworkError) == .notFound })
65+
}
66+
67+
// MARK: - `loadDomainProducts`
68+
69+
func test_loadDomainProducts_returns_products_on_success() async throws {
70+
// Given
71+
let remote = DomainRemote(network: network)
72+
network.simulateResponse(requestUrlSuffix: "products", filename: "domain-products")
73+
74+
// When
75+
// Products are in random order because of the product name mapping.
76+
// They are sorted here to ensure the same order for unit testing.
77+
let products = try await remote.loadDomainProducts().sorted(by: { $0.productID < $1.productID })
78+
79+
// Then
80+
XCTAssertEqual(products, [
81+
.init(productID: 355, term: "year", cost: "US$15.00", saleCost: "US$3.90"),
82+
.init(productID: 356, term: "year", cost: "US$60.00", saleCost: nil)
83+
])
84+
}
85+
86+
func test_loadDomainProducts_returns_error_on_empty_response() async throws {
87+
// Given
88+
let remote = DomainRemote(network: network)
89+
90+
await assertThrowsError({_ = try await remote.loadDomainProducts()}, errorAssert: { ($0 as? NetworkError) == .notFound })
91+
}
92+
4393
// MARK: - `loadDomains`
4494

4595
func test_loadDomains_returns_domains_on_success() async throws {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"dotart_domain": {
3+
"product_id": 355,
4+
"product_name": ".art Domain Registration",
5+
"product_slug": "dotart_domain",
6+
"description": "",
7+
"product_type": "domain_reg",
8+
"available": true,
9+
"billing_product_slug": "wp-dot-art-registration",
10+
"is_domain_registration": true,
11+
"cost_display": "US$2.00",
12+
"combined_cost_display": "US$15.00",
13+
"cost": 2,
14+
"cost_smallest_unit": 200,
15+
"currency_code": "USD",
16+
"price_tier_list": [],
17+
"price_tier_usage_quantity": null,
18+
"product_term": "year",
19+
"price_tiers": [],
20+
"price_tier_slug": "",
21+
"tld": "art",
22+
"is_privacy_protection_product_purchase_allowed": true,
23+
"sale_cost": 3.9,
24+
"combined_sale_cost_display": "US$3.90",
25+
"sale_coupon": {
26+
"start_date": "2022-12-01 00:01:00",
27+
"expires": "2023-03-31 00:00:00",
28+
"discount": 74,
29+
"purchase_types": [
30+
3
31+
],
32+
"product_ids": [
33+
355
34+
],
35+
"allowed_for_domain_transfers": false,
36+
"allowed_for_renewals": false,
37+
"allowed_for_new_purchases": true,
38+
"code": "7ea7af1f76879264",
39+
"tld_rank": null
40+
}
41+
},
42+
"dotbar_domain": {
43+
"product_id": 356,
44+
"product_name": ".bar Domain Registration",
45+
"product_slug": "dotbar_domain",
46+
"description": "",
47+
"product_type": "domain_reg",
48+
"available": true,
49+
"billing_product_slug": "wp-dot-bar-registration",
50+
"is_domain_registration": true,
51+
"cost_display": "US$47.00",
52+
"combined_cost_display": "US$60.00",
53+
"cost": 47,
54+
"cost_smallest_unit": 4700,
55+
"currency_code": "USD",
56+
"price_tier_list": [],
57+
"price_tier_usage_quantity": null,
58+
"product_term": "year",
59+
"price_tiers": [],
60+
"price_tier_slug": "",
61+
"tld": "bar",
62+
"is_privacy_protection_product_purchase_allowed": true
63+
}
64+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[
2+
{
3+
"domain_name": "color.bar",
4+
"relevance": 0.33,
5+
"supports_privacy": true,
6+
"vendor": "donuts",
7+
"match_reasons": [
8+
"exact-match"
9+
],
10+
"product_id": 356,
11+
"product_slug": "dotbar_domain",
12+
"cost": "US$60.00",
13+
"raw_price": 60,
14+
"currency_code": "USD"
15+
},
16+
{
17+
"domain_name": "color.ink",
18+
"relevance": 0.33,
19+
"supports_privacy": true,
20+
"vendor": "donuts",
21+
"match_reasons": [
22+
"exact-match"
23+
],
24+
"product_id": 359,
25+
"product_slug": "dotink_domain",
26+
"cost": "US$25.00",
27+
"raw_price": 25,
28+
"currency_code": "USD"
29+
}
30+
]

Yosemite/YosemiteTests/Mocks/Networking/Remote/MockDomainRemote.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ extension MockDomainRemote: DomainRemoteProtocol {
3030
return try result.get()
3131
}
3232

33+
func loadPaidDomainSuggestions(query: String) async throws -> [PaidDomainSuggestion] {
34+
// TODO: 8558 - Yosemite layer for paid domains
35+
throw NetworkError.notFound
36+
}
37+
38+
func loadDomainProducts() async throws -> [DomainProduct] {
39+
// TODO: 8558 - Yosemite layer for paid domains
40+
throw NetworkError.notFound
41+
}
42+
3343
func loadDomains(siteID: Int64) async throws -> [SiteDomain] {
3444
guard let result = loadDomainsResult else {
3545
XCTFail("Could not find result for loading domains.")

0 commit comments

Comments
 (0)