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
12 changes: 12 additions & 0 deletions Networking/Networking.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
02AF07EA27492DBC00B2D81E /* WordPressMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AF07E927492DBC00B2D81E /* WordPressMedia.swift */; };
02AF07EC27492FDD00B2D81E /* media-library-from-wordpress-site.json in Resources */ = {isa = PBXBuildFile; fileRef = 02AF07EB27492FDD00B2D81E /* media-library-from-wordpress-site.json */; };
02AF07EE27493AE700B2D81E /* media-upload-to-wordpress-site.json in Resources */ = {isa = PBXBuildFile; fileRef = 02AF07ED27493AE700B2D81E /* media-upload-to-wordpress-site.json */; };
02B41A90296BC85800FE3311 /* site-domains.json in Resources */ = {isa = PBXBuildFile; fileRef = 02B41A8F296BC85800FE3311 /* site-domains.json */; };
02B41A92296BEB3000FE3311 /* load-site-current-plan-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 02B41A91296BEB3000FE3311 /* load-site-current-plan-success.json */; };
02B41A94296C04BC00FE3311 /* load-site-plans-no-current-plan.json in Resources */ = {isa = PBXBuildFile; fileRef = 02B41A93296C04BC00FE3311 /* load-site-plans-no-current-plan.json */; };
02BA23C922EEF62C009539E7 /* order-stats-v4-wcadmin-deactivated.json in Resources */ = {isa = PBXBuildFile; fileRef = 02BA23C722EEF62C009539E7 /* order-stats-v4-wcadmin-deactivated.json */; };
02BA23CA22EEF62C009539E7 /* order-stats-v4-wcadmin-activated.json in Resources */ = {isa = PBXBuildFile; fileRef = 02BA23C822EEF62C009539E7 /* order-stats-v4-wcadmin-activated.json */; };
02BDB83523EA98C800BCC63E /* String+HTML.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BDB83423EA98C800BCC63E /* String+HTML.swift */; };
Expand Down Expand Up @@ -880,6 +883,9 @@
02AF07E927492DBC00B2D81E /* WordPressMedia.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressMedia.swift; sourceTree = "<group>"; };
02AF07EB27492FDD00B2D81E /* media-library-from-wordpress-site.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "media-library-from-wordpress-site.json"; sourceTree = "<group>"; };
02AF07ED27493AE700B2D81E /* media-upload-to-wordpress-site.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "media-upload-to-wordpress-site.json"; sourceTree = "<group>"; };
02B41A8F296BC85800FE3311 /* site-domains.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "site-domains.json"; sourceTree = "<group>"; };
02B41A91296BEB3000FE3311 /* load-site-current-plan-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "load-site-current-plan-success.json"; sourceTree = "<group>"; };
02B41A93296C04BC00FE3311 /* load-site-plans-no-current-plan.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "load-site-plans-no-current-plan.json"; sourceTree = "<group>"; };
02BA23C722EEF62C009539E7 /* order-stats-v4-wcadmin-deactivated.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "order-stats-v4-wcadmin-deactivated.json"; sourceTree = "<group>"; };
02BA23C822EEF62C009539E7 /* order-stats-v4-wcadmin-activated.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "order-stats-v4-wcadmin-activated.json"; sourceTree = "<group>"; };
02BDB83423EA98C800BCC63E /* String+HTML.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+HTML.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2155,7 +2161,10 @@
DE50295F28C609A300551736 /* jetpack-connected-user.json */,
DE34051A28BDF12C00CF0D97 /* jetpack-connection-url.json */,
DE50296228C609DE00551736 /* jetpack-user-not-connected.json */,
02B41A91296BEB3000FE3311 /* load-site-current-plan-success.json */,
02B41A93296C04BC00FE3311 /* load-site-plans-no-current-plan.json */,
EE8A86F0286C5226003E8AA4 /* media-update-product-id-in-wordpress-site.json */,
02B41A8F296BC85800FE3311 /* site-domains.json */,
D865CE6D278CC19A002C8520 /* stripe-location-error.json */,
D865CE6C278CC19A002C8520 /* stripe-location.json */,
D865CE6A278CA266002C8520 /* stripe-payment-intent-error.json */,
Expand Down Expand Up @@ -2951,6 +2960,7 @@
743E84FC22174CE100FAC9D7 /* restnoroute_error.json in Resources */,
CE20179320E3EFA7005B4C18 /* broken-orders.json in Resources */,
D8FBFF2722D529F2006E3336 /* order-stats-v4-month.json in Resources */,
02B41A92296BEB3000FE3311 /* load-site-current-plan-success.json in Resources */,
02EF1672292F0D1900D90AD6 /* load-plan-success.json in Resources */,
2685C0DE263B5A4200D9EE97 /* add-on-groups.json in Resources */,
DEC51A9B274E3206009F3DF4 /* plugin-inactive.json in Resources */,
Expand All @@ -2962,6 +2972,7 @@
DE5CA111288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json in Resources */,
02DD6492248A3EC00082523E /* product-external.json in Resources */,
03DCB7522624B3BE00C8953D /* coupons-all.json in Resources */,
02B41A94296C04BC00FE3311 /* load-site-plans-no-current-plan.json in Resources */,
028CB716290223CB00331C09 /* create-account-success.json in Resources */,
036563E129069D3500D84BFD /* just-in-time-message-list.json in Resources */,
45A4B85C25D2FAB500776FB4 /* shipping-label-address-validation-error.json in Resources */,
Expand Down Expand Up @@ -3117,6 +3128,7 @@
2683D71024456EE4002A1589 /* categories-extra.json in Resources */,
A69FE19D2588D70E0059A96B /* order-with-deleted-refunds.json in Resources */,
E18152C228F85E0A0011A0EC /* iap-products.json in Resources */,
02B41A90296BC85800FE3311 /* site-domains.json in Resources */,
EEA658462966C67C00112DF0 /* products-ids-only-without-data.json in Resources */,
DE2095C127966EC800171F1C /* coupon-reports.json in Resources */,
453305F52459ED2700264E50 /* site-post-update.json in Resources */,
Expand Down
44 changes: 43 additions & 1 deletion Networking/Networking/Remote/DomainRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ public protocol DomainRemoteProtocol {
/// - Parameter query: What the domain suggestions are based on.
/// - Returns: The result of free domain suggestions.
func loadFreeDomainSuggestions(query: String) async throws -> [FreeDomainSuggestion]

/// Loads all domains for a site.
/// - Parameter siteID: ID of the site to load the domains for.
/// - Returns: A list of domains.
func loadDomains(siteID: Int64) async throws -> [SiteDomain]
}

/// Domain: Remote Endpoints
Expand All @@ -21,6 +26,13 @@ public class DomainRemote: Remote, DomainRemoteProtocol {
let request = DotcomRequest(wordpressApiVersion: .mark1_1, method: .get, path: path, parameters: parameters)
return try await enqueue(request)
}

public func loadDomains(siteID: Int64) async throws -> [SiteDomain] {
let path = "sites/\(siteID)/\(Path.domains)"
let request = DotcomRequest(wordpressApiVersion: .mark1_1, method: .get, path: path)
let response: SiteDomainEnvelope = try await enqueue(request)
return response.domains
}
}

/// Necessary data for a free domain suggestion.
Expand All @@ -42,7 +54,7 @@ public struct FreeDomainSuggestion: Decodable, Equatable {
}

/// Necessary data for a site's domain.
public struct SiteDomain: Equatable {
public struct SiteDomain: Decodable, Equatable {
/// Domain name.
public let name: String

Expand All @@ -57,6 +69,35 @@ public struct SiteDomain: Equatable {
self.isPrimary = isPrimary
self.renewalDate = renewalDate
}

/// Custom decoding implementation since `renewalDate` is an empty string instead of `null` when it's unavailable.
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let name = try container.decode(String.self, forKey: .name)
let isPrimary = try container.decode(Bool.self, forKey: .isPrimary)

let renewalDate: Date? = {
guard let dateString = try? container.decodeIfPresent(String.self, forKey: .renewalDate) else {
return nil
}
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMMM d, yyyy"
return dateFormatter.date(from: dateString)
}()

self.init(name: name, isPrimary: isPrimary, renewalDate: renewalDate)
}

private enum CodingKeys: String, CodingKey {
case name = "domain"
case isPrimary = "primary_domain"
case renewalDate = "auto_renewal_date"
}
}

/// Maps to a list of domains to match the API response.
private struct SiteDomainEnvelope: Decodable {
let domains: [SiteDomain]
}

// MARK: - Constants
Expand All @@ -77,5 +118,6 @@ private extension DomainRemote {

enum Path {
static let domainSuggestions = "domains/suggestions"
static let domains = "domains"
}
}
39 changes: 34 additions & 5 deletions Networking/Networking/Remote/PaymentRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ public protocol PaymentRemoteProtocol {
/// - Returns: The WPCOM plan that matches the given product ID.
func loadPlan(thatMatchesID productID: Int64) async throws -> WPComPlan

/// Loads the current WPCOM plan of a site.
/// - Parameter siteID: ID of the site to load the current plan for.
/// - Returns: The current WPCOM plan of the given site.
func loadSiteCurrentPlan(siteID: Int64) async throws -> WPComSitePlan

/// Creates a cart with the given product ID for the site ID.
/// - Parameters:
/// - siteID: The ID of the site that the product is being added to.
Expand All @@ -28,6 +33,16 @@ public class PaymentRemote: Remote, PaymentRemoteProtocol {
return plan
}

public func loadSiteCurrentPlan(siteID: Int64) async throws -> WPComSitePlan {
let path = "sites/\(siteID)/\(Path.products)"
let request = DotcomRequest(wordpressApiVersion: .mark1_3, method: .get, path: path)
let plansByID: [String: SiteCurrentPlanResponse] = try await enqueue(request)
guard let currentPlan = plansByID.values.filter({ $0.isCurrentPlan == true }).first else {
throw LoadSiteCurrentPlanError.noCurrentPlan
}
return .init(hasDomainCredit: currentPlan.hasDomainCredit ?? false)
}

public func createCart(siteID: Int64, productID: Int64) async throws {
let path = "\(Path.cartCreation)/\(siteID)"

Expand Down Expand Up @@ -69,14 +84,11 @@ public struct WPComPlan: Decodable, Equatable {
}

/// Contains necessary data for a site's WPCOM plan.
public struct WPComSitePlan {
/// WPCOM plan of a site.
public let plan: WPComPlan
public struct WPComSitePlan: Equatable {
/// Whether a site has domain credit from the WPCOM plan.
public let hasDomainCredit: Bool

public init(plan: WPComPlan, hasDomainCredit: Bool) {
self.plan = plan
public init(hasDomainCredit: Bool) {
self.hasDomainCredit = hasDomainCredit
}
}
Expand All @@ -86,11 +98,28 @@ public enum LoadPlanError: Error {
case noMatchingPlan
}

/// Possible error cases from loading a site's current WPCOM plan.
public enum LoadSiteCurrentPlanError: Error {
case noCurrentPlan
}

/// Possible error cases from creating cart for a site with a WPCOM plan.
public enum CreateCartError: Error {
case productNotInCart
}

/// Contains necessary data for handling the remote response from loading a site's current plan.
/// The fields are all optional because only the current plan has these fields.
private struct SiteCurrentPlanResponse: Decodable {
let isCurrentPlan: Bool?
let hasDomainCredit: Bool?

private enum CodingKeys: String, CodingKey {
case isCurrentPlan = "current_plan"
case hasDomainCredit = "has_domain_credit"
}
}

/// Contains necessary data for handling the remote response from creating a cart.
private struct CreateCartResponse: Decodable {
let products: [Product]
Expand Down
2 changes: 1 addition & 1 deletion Networking/Networking/Requests/DotcomRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ struct DotcomRequest: Request {

func responseDataValidator() -> ResponseDataValidator {
switch wordpressApiVersion {
case .mark1_1, .mark1_2, .mark1_5:
case .mark1_1, .mark1_2, .mark1_3, .mark1_5:
return DotcomValidator()
case .wpcomMark2, .wpMark2:
return WordPressApiValidator()
Expand Down
4 changes: 4 additions & 0 deletions Networking/Networking/Settings/WordPressAPIVersion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ enum WordPressAPIVersion: String {
///
case mark1_2 = "rest/v1.2/"

/// WordPress.com Endpoint Mark 1.3
///
case mark1_3 = "rest/v1.3/"

/// WordPress.com Endpoint Mark 1.5
///
case mark1_5 = "rest/v1.5/"
Expand Down
30 changes: 30 additions & 0 deletions Networking/NetworkingTests/Remote/DomainRemoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ final class DomainRemoteTests: XCTestCase {
super.tearDown()
}

// MARK: - `loadFreeDomainSuggestions`

func test_loadFreeDomainSuggestions_returns_suggestions_on_success() async throws {
// Given
let remote = DomainRemote(network: network)
Expand All @@ -37,4 +39,32 @@ final class DomainRemoteTests: XCTestCase {

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

// MARK: - `loadDomains`

func test_loadDomains_returns_domains_on_success() async throws {
// Given
let remote = DomainRemote(network: network)
network.simulateResponse(requestUrlSuffix: "domains", filename: "site-domains")

// When
let domains = try await remote.loadDomains(siteID: 23)

// Then
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMMM d, yyyy"
let renewalDate = try XCTUnwrap(dateFormatter.date(from: "December 10, 2023"))
XCTAssertEqual(domains, [
.init(name: "crabparty.wpcomstaging.com", isPrimary: true),
.init(name: "crabparty.com", isPrimary: false, renewalDate: renewalDate),
.init(name: "crabparty.wordpress.com", isPrimary: false)
])
}

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

await assertThrowsError({_ = try await remote.loadDomains(siteID: 23)}, errorAssert: { ($0 as? NetworkError) == .notFound })
}
}
41 changes: 41 additions & 0 deletions Networking/NetworkingTests/Remote/PaymentRemoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,47 @@ final class PaymentRemoteTests: XCTestCase {
}
}

// MARK: - `loadSiteCurrentPlan`

func test_loadSiteCurrentPlan_returns_site_plan_on_success() async throws {
// Given
let remote = PaymentRemote(network: network)
network.simulateResponse(requestUrlSuffix: "plans", filename: "load-site-current-plan-success")

// When
let plan = try await remote.loadSiteCurrentPlan(siteID: 134)

// Then
XCTAssertEqual(plan, .init(hasDomainCredit: false))
}

func test_loadSiteCurrentPlan_returns_noCurrentPlan_error_when_response_has_no_current_plan() async throws {
// Given
let remote = PaymentRemote(network: network)
network.simulateResponse(requestUrlSuffix: "plans", filename: "load-site-plans-no-current-plan")

// When
await assertThrowsError {
_ = try await remote.loadSiteCurrentPlan(siteID: 6)
} errorAssert: { error in
// Then
(error as? LoadSiteCurrentPlanError) == LoadSiteCurrentPlanError.noCurrentPlan
}
}

func test_loadSiteCurrentPlan_throws_notFound_error_when_no_response() async throws {
// Given
let remote = PaymentRemote(network: network)

// When
await assertThrowsError {
_ = try await remote.loadSiteCurrentPlan(siteID: 6)
} errorAssert: { error in
// Then
(error as? NetworkError) == .notFound
}
}

// MARK: - `createCart`

func test_createCart_returns_on_success() async throws {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"1": {
"formatted_original_price": "US$0",
"raw_price": 0,
"formatted_price": "US$0",
"raw_discount": 0,
"formatted_discount": "US$0",
"product_slug": "free_plan",
"product_name": "WordPress.com Free",
"discount_reason": null,
"is_domain_upgrade": null,
"currency_code": "USD",
"interval": -1
},
"1008": {
"formatted_original_price": "US$0",
"raw_price": 300,
"formatted_price": "US$300",
"raw_discount": 0,
"formatted_discount": "US$0",
"product_slug": "business-bundle",
"product_name": "WordPress.com Business",
"discount_reason": null,
"is_domain_upgrade": null,
"currency_code": "USD",
"current_plan": true,
"user_is_owner": true,
"id": "",
"has_domain_credit": false,
"expiry": "2025-01-01T00:00:00+00:00",
"free_trial": false,
"subscribed_date": "2019-04-29T04:32:29+00:00",
"auto_renew": false,
"auto_renew_date": "2024-12-02T00:00:00+00:00",
"partner_name": "",
"user_facing_expiry": "2025-01-01T00:00:00+00:00",
"interval": 365
},
"1011": {
"formatted_original_price": "US$540",
"raw_price": 240,
"formatted_price": "US$240",
"raw_discount": 300,
"formatted_discount": "US$300",
"product_slug": "ecommerce-bundle",
"product_name": "WordPress.com eCommerce",
"discount_reason": "Your recent plan purchase is deducted from the price (US$540).",
"is_domain_upgrade": false,
"currency_code": "USD",
"can_start_trial": false,
"interval": 365
}
}
Loading