From 00c78cb39cdabd29720184fd75cafc1437c165cc Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 5 May 2025 10:15:50 -0400 Subject: [PATCH 01/17] Add search to subscribers --- Package.swift | 4 +-- .../Services/PeopleServiceRemote.swift | 32 ++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Package.swift b/Package.swift index 3172714e..24a5c41f 100644 --- a/Package.swift +++ b/Package.swift @@ -11,8 +11,8 @@ let package = Package( targets: [ .binaryTarget( name: "WordPressKit", - url: "https://github.com/user-attachments/files/19949939/WordPressKit.zip", - checksum: "ba06ff0716595023dd6c98b6a5bc74d4abb35bfb668e24026ffd041460f59137" + url: "https://github.com/user-attachments/files/20038416/WordPressKit.zip", + checksum: "fa431397e9124b49562fbc5b4f0dccd238fe28f8744826e9a26d14ca57860173" ), ] ) diff --git a/Sources/WordPressKit/Services/PeopleServiceRemote.swift b/Sources/WordPressKit/Services/PeopleServiceRemote.swift index 5c40228b..36856a83 100644 --- a/Sources/WordPressKit/Services/PeopleServiceRemote.swift +++ b/Sources/WordPressKit/Services/PeopleServiceRemote.swift @@ -173,10 +173,11 @@ public class PeopleServiceRemote: ServiceRemoteWordPressComREST { }) } - public struct SubscribersParameters { + public struct SubscribersParameters: Hashable { public var sortField: SortField? public var sortOrder: SortOrder? - public var filters: [Filter] + public var filters: Set + public var search: String? public enum SortField: String { case dateSubscribed = "date_subscribed" @@ -191,25 +192,31 @@ public class PeopleServiceRemote: ServiceRemoteWordPressComREST { case descending = "dsc" } - public protocol Filter: CustomStringConvertible {} + public enum Filter: Hashable { + case subscription(FilterSubscriptionType) + case payment(FilterPaymentType) - public enum FilterSubscriptionType: String, Filter { + var rawValue: String { + switch self { + case .subscription(let filter): filter.rawValue + case .payment(let filter): filter.rawValue + } + } + } + + public enum FilterSubscriptionType: String { case email = "email_subscriber" case reader = "reader_subscriber" case unconfirmed = "unconfirmed_subscriber" case blocked = "blocked_subscriber" - - public var description: String { rawValue } } - public enum FilterPaymentType: String, Filter { + public enum FilterPaymentType: String { case free case paid - - public var description: String { rawValue } } - public init(sortField: SortField? = nil, sortOrder: SortOrder? = nil, filters: [Filter] = []) { + public init(sortField: SortField? = nil, sortOrder: SortOrder? = nil, filters: Set = []) { self.sortField = sortField self.sortOrder = sortOrder self.filters = filters @@ -244,7 +251,10 @@ public class PeopleServiceRemote: ServiceRemoteWordPressComREST { query["sort_order"] = sortOrder.rawValue } if !parameters.filters.isEmpty { - query["filters"] = parameters.filters.map { $0.description } + query["filters"] = parameters.filters.map(\.rawValue) + } + if let search = parameters.search, !search.isEmpty { + query["search"] = search } let decoder = JSONDecoder() From 92c48fcfb40a7f31fe6c0010fdbf9fe459ded223 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 5 May 2025 10:40:38 -0400 Subject: [PATCH 02/17] Add search to initializer --- Package.swift | 4 ++-- Sources/WordPressKit/Services/PeopleServiceRemote.swift | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 24a5c41f..eca0bd2c 100644 --- a/Package.swift +++ b/Package.swift @@ -11,8 +11,8 @@ let package = Package( targets: [ .binaryTarget( name: "WordPressKit", - url: "https://github.com/user-attachments/files/20038416/WordPressKit.zip", - checksum: "fa431397e9124b49562fbc5b4f0dccd238fe28f8744826e9a26d14ca57860173" + url: "https://github.com/user-attachments/files/20038728/WordPressKit.zip", + checksum: "7361289ba2ccef75d47dc8c6074072295812314b83194407864575faef1ae140" ), ] ) diff --git a/Sources/WordPressKit/Services/PeopleServiceRemote.swift b/Sources/WordPressKit/Services/PeopleServiceRemote.swift index 36856a83..ca12a838 100644 --- a/Sources/WordPressKit/Services/PeopleServiceRemote.swift +++ b/Sources/WordPressKit/Services/PeopleServiceRemote.swift @@ -216,10 +216,11 @@ public class PeopleServiceRemote: ServiceRemoteWordPressComREST { case paid } - public init(sortField: SortField? = nil, sortOrder: SortOrder? = nil, filters: Set = []) { + public init(sortField: SortField? = nil, sortOrder: SortOrder? = nil, filters: Set = [], search: String? = nil) { self.sortField = sortField self.sortOrder = sortOrder self.filters = filters + self.search = search } } From bb34b0fe64c15795cc1cf68ce514c2da2f77a7bd Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 5 May 2025 11:18:03 -0400 Subject: [PATCH 03/17] Fix missing query --- Package.swift | 4 ++-- Sources/WordPressKit/Services/PeopleServiceRemote.swift | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index eca0bd2c..ac301dac 100644 --- a/Package.swift +++ b/Package.swift @@ -11,8 +11,8 @@ let package = Package( targets: [ .binaryTarget( name: "WordPressKit", - url: "https://github.com/user-attachments/files/20038728/WordPressKit.zip", - checksum: "7361289ba2ccef75d47dc8c6074072295812314b83194407864575faef1ae140" + url: "https://github.com/user-attachments/files/20039232/WordPressKit.zip", + checksum: "6d0d9f96dbe6d810306e5b00635d65c61ae2a0409da519c462bf18f28801db55" ), ] ) diff --git a/Sources/WordPressKit/Services/PeopleServiceRemote.swift b/Sources/WordPressKit/Services/PeopleServiceRemote.swift index ca12a838..fad58f76 100644 --- a/Sources/WordPressKit/Services/PeopleServiceRemote.swift +++ b/Sources/WordPressKit/Services/PeopleServiceRemote.swift @@ -264,6 +264,7 @@ public class PeopleServiceRemote: ServiceRemoteWordPressComREST { return try await wordPressComRestApi.perform( .get, URLString: url, + parameters: query, jsonDecoder: decoder, type: SubscribersResponse.self ).get().body From 40342e8945e143dfa290bd1d503ebf22f577a1ff Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 6 May 2025 09:05:50 -0400 Subject: [PATCH 04/17] Simplify how filters are passed --- Package.swift | 4 +-- .../Services/PeopleServiceRemote.swift | 33 ++++++++----------- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/Package.swift b/Package.swift index ac301dac..432ef449 100644 --- a/Package.swift +++ b/Package.swift @@ -11,8 +11,8 @@ let package = Package( targets: [ .binaryTarget( name: "WordPressKit", - url: "https://github.com/user-attachments/files/20039232/WordPressKit.zip", - checksum: "6d0d9f96dbe6d810306e5b00635d65c61ae2a0409da519c462bf18f28801db55" + url: "https://github.com/user-attachments/files/20062492/WordPressKit.zip", + checksum: "dd2bcc029d1d0bdc1b973f006d4c5cb9f0a6219a1fbfd82392c28be8c0bbe2a3" ), ] ) diff --git a/Sources/WordPressKit/Services/PeopleServiceRemote.swift b/Sources/WordPressKit/Services/PeopleServiceRemote.swift index fad58f76..a73b37db 100644 --- a/Sources/WordPressKit/Services/PeopleServiceRemote.swift +++ b/Sources/WordPressKit/Services/PeopleServiceRemote.swift @@ -176,8 +176,8 @@ public class PeopleServiceRemote: ServiceRemoteWordPressComREST { public struct SubscribersParameters: Hashable { public var sortField: SortField? public var sortOrder: SortOrder? - public var filters: Set - public var search: String? + public var subscriptionTypeFilter: FilterSubscriptionType? + public var paymentTypeFilter: FilterPaymentType? public enum SortField: String { case dateSubscribed = "date_subscribed" @@ -192,18 +192,6 @@ public class PeopleServiceRemote: ServiceRemoteWordPressComREST { case descending = "dsc" } - public enum Filter: Hashable { - case subscription(FilterSubscriptionType) - case payment(FilterPaymentType) - - var rawValue: String { - switch self { - case .subscription(let filter): filter.rawValue - case .payment(let filter): filter.rawValue - } - } - } - public enum FilterSubscriptionType: String { case email = "email_subscriber" case reader = "reader_subscriber" @@ -216,11 +204,15 @@ public class PeopleServiceRemote: ServiceRemoteWordPressComREST { case paid } - public init(sortField: SortField? = nil, sortOrder: SortOrder? = nil, filters: Set = [], search: String? = nil) { + var filters: [String] { + [subscriptionTypeFilter?.rawValue, paymentTypeFilter?.rawValue].compactMap { $0 } + } + + public init(sortField: SortField? = nil, sortOrder: SortOrder? = nil, subscriptionTypeFilter: FilterSubscriptionType? = nil, paymentTypeFilter: FilterPaymentType? = nil) { self.sortField = sortField self.sortOrder = sortOrder - self.filters = filters - self.search = search + self.subscriptionTypeFilter = subscriptionTypeFilter + self.paymentTypeFilter = paymentTypeFilter } } @@ -235,7 +227,8 @@ public class PeopleServiceRemote: ServiceRemoteWordPressComREST { siteID: Int, page: Int? = nil, perPage: Int? = 25, - parameters: SubscribersParameters = .init() + parameters: SubscribersParameters = .init(), + search: String? = nil, ) async throws -> SubscribersResponse { let url = self.path(forEndpoint: "sites/\(siteID)/subscribers", withVersion: ._2_0) var query: [String: Any] = [:] @@ -252,9 +245,9 @@ public class PeopleServiceRemote: ServiceRemoteWordPressComREST { query["sort_order"] = sortOrder.rawValue } if !parameters.filters.isEmpty { - query["filters"] = parameters.filters.map(\.rawValue) + query["filters"] = parameters.filters } - if let search = parameters.search, !search.isEmpty { + if let search, !search.isEmpty { query["search"] = search } From 8ffb0d4d3624c8271e49e5e335d6bf59884691ea Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 6 May 2025 11:56:09 -0400 Subject: [PATCH 05/17] Add CaseIterable --- Package.swift | 4 ++-- Sources/WordPressKit/Services/PeopleServiceRemote.swift | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Package.swift b/Package.swift index 432ef449..a2ac129f 100644 --- a/Package.swift +++ b/Package.swift @@ -11,8 +11,8 @@ let package = Package( targets: [ .binaryTarget( name: "WordPressKit", - url: "https://github.com/user-attachments/files/20062492/WordPressKit.zip", - checksum: "dd2bcc029d1d0bdc1b973f006d4c5cb9f0a6219a1fbfd82392c28be8c0bbe2a3" + url: "https://github.com/user-attachments/files/20066712/WordPressKit.zip", + checksum: "0663dd7a2608185cdd5cabb99046cd0146d6ef22a446cf8d2c123823d813b813" ), ] ) diff --git a/Sources/WordPressKit/Services/PeopleServiceRemote.swift b/Sources/WordPressKit/Services/PeopleServiceRemote.swift index a73b37db..4763b61b 100644 --- a/Sources/WordPressKit/Services/PeopleServiceRemote.swift +++ b/Sources/WordPressKit/Services/PeopleServiceRemote.swift @@ -179,7 +179,7 @@ public class PeopleServiceRemote: ServiceRemoteWordPressComREST { public var subscriptionTypeFilter: FilterSubscriptionType? public var paymentTypeFilter: FilterPaymentType? - public enum SortField: String { + public enum SortField: String, CaseIterable { case dateSubscribed = "date_subscribed" case email = "email" case name = "name" @@ -187,19 +187,19 @@ public class PeopleServiceRemote: ServiceRemoteWordPressComREST { case subscriptionStatus = "subscription_status" } - public enum SortOrder: String { + public enum SortOrder: String, CaseIterable { case ascending = "asc" case descending = "dsc" } - public enum FilterSubscriptionType: String { + public enum FilterSubscriptionType: String, CaseIterable { case email = "email_subscriber" case reader = "reader_subscriber" case unconfirmed = "unconfirmed_subscriber" case blocked = "blocked_subscriber" } - public enum FilterPaymentType: String { + public enum FilterPaymentType: String, CaseIterable { case free case paid } From 0bd3c27d0b9b690488c8af6e0ac5219ba3ffe417 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 6 May 2025 12:21:57 -0400 Subject: [PATCH 06/17] Add @frozen --- Package.swift | 4 ++-- Sources/WordPressKit/Services/PeopleServiceRemote.swift | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Package.swift b/Package.swift index a2ac129f..9f842f83 100644 --- a/Package.swift +++ b/Package.swift @@ -11,8 +11,8 @@ let package = Package( targets: [ .binaryTarget( name: "WordPressKit", - url: "https://github.com/user-attachments/files/20066712/WordPressKit.zip", - checksum: "0663dd7a2608185cdd5cabb99046cd0146d6ef22a446cf8d2c123823d813b813" + url: "https://github.com/user-attachments/files/20067014/WordPressKit.zip", + checksum: "e20c387a1c32306e502326af03f46629140b9d1bc994de3c614890a0fd24b690" ), ] ) diff --git a/Sources/WordPressKit/Services/PeopleServiceRemote.swift b/Sources/WordPressKit/Services/PeopleServiceRemote.swift index 4763b61b..3f7815eb 100644 --- a/Sources/WordPressKit/Services/PeopleServiceRemote.swift +++ b/Sources/WordPressKit/Services/PeopleServiceRemote.swift @@ -179,7 +179,7 @@ public class PeopleServiceRemote: ServiceRemoteWordPressComREST { public var subscriptionTypeFilter: FilterSubscriptionType? public var paymentTypeFilter: FilterPaymentType? - public enum SortField: String, CaseIterable { + @frozen public enum SortField: String, CaseIterable { case dateSubscribed = "date_subscribed" case email = "email" case name = "name" @@ -187,19 +187,19 @@ public class PeopleServiceRemote: ServiceRemoteWordPressComREST { case subscriptionStatus = "subscription_status" } - public enum SortOrder: String, CaseIterable { + @frozen public enum SortOrder: String, CaseIterable { case ascending = "asc" case descending = "dsc" } - public enum FilterSubscriptionType: String, CaseIterable { + @frozen public enum FilterSubscriptionType: String, CaseIterable { case email = "email_subscriber" case reader = "reader_subscriber" case unconfirmed = "unconfirmed_subscriber" case blocked = "blocked_subscriber" } - public enum FilterPaymentType: String, CaseIterable { + @frozen public enum FilterPaymentType: String, CaseIterable { case free case paid } From dc2731551d0139df143dea6b4431475b3bba65a2 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 7 May 2025 11:38:00 -0400 Subject: [PATCH 07/17] Extract SubscribersServiceRemote --- .../Services/PeopleServiceRemote.swift | 90 ----------------- .../Services/SubscribersServiceRemote.swift | 96 +++++++++++++++++++ .../Tests/PeopleServiceRemoteTests.swift | 14 --- .../Tests/SubscribersServiceRemoteTests.swift | 19 ++++ WordPressKit.xcodeproj/project.pbxproj | 8 ++ 5 files changed, 123 insertions(+), 104 deletions(-) create mode 100644 Sources/WordPressKit/Services/SubscribersServiceRemote.swift create mode 100644 Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift diff --git a/Sources/WordPressKit/Services/PeopleServiceRemote.swift b/Sources/WordPressKit/Services/PeopleServiceRemote.swift index 3f7815eb..e23b26ae 100644 --- a/Sources/WordPressKit/Services/PeopleServiceRemote.swift +++ b/Sources/WordPressKit/Services/PeopleServiceRemote.swift @@ -173,96 +173,6 @@ public class PeopleServiceRemote: ServiceRemoteWordPressComREST { }) } - public struct SubscribersParameters: Hashable { - public var sortField: SortField? - public var sortOrder: SortOrder? - public var subscriptionTypeFilter: FilterSubscriptionType? - public var paymentTypeFilter: FilterPaymentType? - - @frozen public enum SortField: String, CaseIterable { - case dateSubscribed = "date_subscribed" - case email = "email" - case name = "name" - case plan = "plan" - case subscriptionStatus = "subscription_status" - } - - @frozen public enum SortOrder: String, CaseIterable { - case ascending = "asc" - case descending = "dsc" - } - - @frozen public enum FilterSubscriptionType: String, CaseIterable { - case email = "email_subscriber" - case reader = "reader_subscriber" - case unconfirmed = "unconfirmed_subscriber" - case blocked = "blocked_subscriber" - } - - @frozen public enum FilterPaymentType: String, CaseIterable { - case free - case paid - } - - var filters: [String] { - [subscriptionTypeFilter?.rawValue, paymentTypeFilter?.rawValue].compactMap { $0 } - } - - public init(sortField: SortField? = nil, sortOrder: SortOrder? = nil, subscriptionTypeFilter: FilterSubscriptionType? = nil, paymentTypeFilter: FilterPaymentType? = nil) { - self.sortField = sortField - self.sortOrder = sortOrder - self.subscriptionTypeFilter = subscriptionTypeFilter - self.paymentTypeFilter = paymentTypeFilter - } - } - - public struct SubscribersResponse: Decodable { - public var total: Int - public var pages: Int - public var page: Int - public var subscribers: [RemoteSubscriber] - } - - public func getSubscribers( - siteID: Int, - page: Int? = nil, - perPage: Int? = 25, - parameters: SubscribersParameters = .init(), - search: String? = nil, - ) async throws -> SubscribersResponse { - let url = self.path(forEndpoint: "sites/\(siteID)/subscribers", withVersion: ._2_0) - var query: [String: Any] = [:] - if let page { - query["page"] = page - } - if let perPage { - query["per_page"] = perPage - } - if let sortField = parameters.sortField { - query["sort"] = sortField.rawValue - } - if let sortOrder = parameters.sortOrder { - query["sort_order"] = sortOrder.rawValue - } - if !parameters.filters.isEmpty { - query["filters"] = parameters.filters - } - if let search, !search.isEmpty { - query["search"] = search - } - - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.supportMultipleDateFormats - - return try await wordPressComRestApi.perform( - .get, - URLString: url, - parameters: query, - jsonDecoder: decoder, - type: SubscribersResponse.self - ).get().body - } - /// Updates a specified User's Role /// /// - Parameters: diff --git a/Sources/WordPressKit/Services/SubscribersServiceRemote.swift b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift new file mode 100644 index 00000000..9c4d552f --- /dev/null +++ b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift @@ -0,0 +1,96 @@ +import Foundation + +public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { + + // MARK: GET + + public struct GetSubscribersParameters: Hashable { + public var sortField: SortField? + public var sortOrder: SortOrder? + public var subscriptionTypeFilter: FilterSubscriptionType? + public var paymentTypeFilter: FilterPaymentType? + + @frozen public enum SortField: String, CaseIterable { + case dateSubscribed = "date_subscribed" + case email = "email" + case name = "name" + case plan = "plan" + case subscriptionStatus = "subscription_status" + } + + @frozen public enum SortOrder: String, CaseIterable { + case ascending = "asc" + case descending = "dsc" + } + + @frozen public enum FilterSubscriptionType: String, CaseIterable { + case email = "email_subscriber" + case reader = "reader_subscriber" + case unconfirmed = "unconfirmed_subscriber" + case blocked = "blocked_subscriber" + } + + @frozen public enum FilterPaymentType: String, CaseIterable { + case free + case paid + } + + var filters: [String] { + [subscriptionTypeFilter?.rawValue, paymentTypeFilter?.rawValue].compactMap { $0 } + } + + public init(sortField: SortField? = nil, sortOrder: SortOrder? = nil, subscriptionTypeFilter: FilterSubscriptionType? = nil, paymentTypeFilter: FilterPaymentType? = nil) { + self.sortField = sortField + self.sortOrder = sortOrder + self.subscriptionTypeFilter = subscriptionTypeFilter + self.paymentTypeFilter = paymentTypeFilter + } + } + + public struct GetSubscribersResponse: Decodable { + public var total: Int + public var pages: Int + public var page: Int + public var subscribers: [RemoteSubscriber] + } + + public func getSubscribers( + siteID: Int, + page: Int? = nil, + perPage: Int? = 25, + parameters: GetSubscribersParameters = .init(), + search: String? = nil, + ) async throws -> GetSubscribersResponse { + let url = self.path(forEndpoint: "sites/\(siteID)/subscribers", withVersion: ._2_0) + var query: [String: Any] = [:] + if let page { + query["page"] = page + } + if let perPage { + query["per_page"] = perPage + } + if let sortField = parameters.sortField { + query["sort"] = sortField.rawValue + } + if let sortOrder = parameters.sortOrder { + query["sort_order"] = sortOrder.rawValue + } + if !parameters.filters.isEmpty { + query["filters"] = parameters.filters + } + if let search, !search.isEmpty { + query["search"] = search + } + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.supportMultipleDateFormats + + return try await wordPressComRestApi.perform( + .get, + URLString: url, + parameters: query, + jsonDecoder: decoder, + type: GetSubscribersResponse.self + ).get().body + } +} diff --git a/Tests/WordPressKitTests/Tests/PeopleServiceRemoteTests.swift b/Tests/WordPressKitTests/Tests/PeopleServiceRemoteTests.swift index 1df76fb6..755a2068 100644 --- a/Tests/WordPressKitTests/Tests/PeopleServiceRemoteTests.swift +++ b/Tests/WordPressKitTests/Tests/PeopleServiceRemoteTests.swift @@ -794,18 +794,4 @@ class PeopleServiceRemoteTests: RemoteTestCase, RESTTestable { waitForExpectations(timeout: timeout, handler: nil) } - - func testDecodeSubscribersResponse() throws { - let data = try JSONLoader.data(named: "site-subscribers-response") - - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.supportMultipleDateFormats - - let response = try decoder.decode(PeopleServiceRemote.SubscribersResponse.self, from: data) - - XCTAssertEqual(response.total, 1) - - let subscriber = try XCTUnwrap(response.subscribers.first) - XCTAssertEqual(subscriber.userID, 1) - } } diff --git a/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift b/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift new file mode 100644 index 00000000..97c2ea05 --- /dev/null +++ b/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift @@ -0,0 +1,19 @@ +import Foundation +import XCTest +@testable import WordPressKit + +class SubscribersServiceRemoteTests: RemoteTestCase, RESTTestable { + func testDecodeSubscribersResponse() throws { + let data = try JSONLoader.data(named: "site-subscribers-response") + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.supportMultipleDateFormats + + let response = try decoder.decode(SubscribersServiceRemote.GetSubscribersResponse.self, from: data) + + XCTAssertEqual(response.total, 1) + + let subscriber = try XCTUnwrap(response.subscribers.first) + XCTAssertEqual(subscriber.userID, 1) + } +} diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index f5ef8417..df79b6a4 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -51,6 +51,8 @@ 0CCD4C5C2C41700B00B53F9A /* UIDevice+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCD4C5B2C41700B00B53F9A /* UIDevice+Extensions.swift */; }; 0CCD4C5F2C41711800B53F9A /* NSObject-SafeExpectations in Frameworks */ = {isa = PBXBuildFile; productRef = 0CCD4C5E2C41711800B53F9A /* NSObject-SafeExpectations */; }; 0CCD4C622C41712800B53F9A /* wpxmlrpc in Frameworks */ = {isa = PBXBuildFile; productRef = 0CCD4C612C41712800B53F9A /* wpxmlrpc */; }; + 0CE311BD2DCBB52C003AADB3 /* SubscribersServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE311BC2DCBB52C003AADB3 /* SubscribersServiceRemote.swift */; }; + 0CE311BF2DCBB588003AADB3 /* SubscribersServiceRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE311BE2DCBB588003AADB3 /* SubscribersServiceRemoteTests.swift */; }; 0CE4E8252DC027AC00056DD9 /* RemoteSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE4E8242DC027AC00056DD9 /* RemoteSubscriber.swift */; }; 0CED1FE82B617CF300E6DD52 /* AtomicSiteServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CED1FE72B617CF300E6DD52 /* AtomicSiteServiceRemote.swift */; }; 0CED1FEB2B617D7D00E6DD52 /* AtomicLogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CED1FEA2B617D7D00E6DD52 /* AtomicLogs.swift */; }; @@ -827,6 +829,8 @@ 0CB1905F2A2A6943004D3E80 /* blaze-campaigns-search.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blaze-campaigns-search.json"; sourceTree = ""; }; 0CB190642A2A7569004D3E80 /* BlazeCampaignsSearchResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignsSearchResponse.swift; sourceTree = ""; }; 0CCD4C5B2C41700B00B53F9A /* UIDevice+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extensions.swift"; sourceTree = ""; }; + 0CE311BC2DCBB52C003AADB3 /* SubscribersServiceRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribersServiceRemote.swift; sourceTree = ""; }; + 0CE311BE2DCBB588003AADB3 /* SubscribersServiceRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribersServiceRemoteTests.swift; sourceTree = ""; }; 0CE4E8242DC027AC00056DD9 /* RemoteSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSubscriber.swift; sourceTree = ""; }; 0CED1FE72B617CF300E6DD52 /* AtomicSiteServiceRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicSiteServiceRemote.swift; sourceTree = ""; }; 0CED1FEA2B617D7D00E6DD52 /* AtomicLogs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicLogs.swift; sourceTree = ""; }; @@ -1953,6 +1957,7 @@ 74A44DC91F13C533006CD8F4 /* NotificationSyncServiceRemote.swift */, 4625B96B253A357500C04AAD /* PageLayoutServiceRemote.swift */, 74D67F051F1528470010C5ED /* PeopleServiceRemote.swift */, + 0CE311BC2DCBB52C003AADB3 /* SubscribersServiceRemote.swift */, 3F3195AB266FF91100397EE7 /* Plans */, C79719682679007B0072F984 /* Plugin Management */, E1BD95141FD5A2B800CD5CE3 /* PluginDirectoryServiceRemote.swift */, @@ -2081,6 +2086,7 @@ 74A44DD31F13C6D8006CD8F4 /* PushAuthenticationServiceRemoteTests.swift */, 74FC6F3A1F191BB400112505 /* NotificationSyncServiceRemoteTests.swift */, 74D67F091F15C24C0010C5ED /* PeopleServiceRemoteTests.swift */, + 0CE311BE2DCBB588003AADB3 /* SubscribersServiceRemoteTests.swift */, 7433BC031EFC4556002D9E92 /* PlanServiceRemoteTests.swift */, E13EE14B1F332C4400C15787 /* PluginServiceRemoteTests.swift */, E1E89C691FD6BDB1006E7A33 /* PluginDirectoryTests.swift */, @@ -3446,6 +3452,7 @@ 4624222D2548BA0F002B8A12 /* RemoteSiteDesign.swift in Sources */, 74D67F061F1528470010C5ED /* PeopleServiceRemote.swift in Sources */, 0C938A2B2C416DE0009BA7B2 /* DisplayableImageHelper.m in Sources */, + 0CE311BD2DCBB52C003AADB3 /* SubscribersServiceRemote.swift in Sources */, 98DC787522BAEBF200267279 /* StatsAllAnnualInsight.swift in Sources */, 740B23C31F17EE8000067A2A /* RemotePostCategory.m in Sources */, 8B2F4BF124ACE3C30056C08A /* RemoteReaderInterest.swift in Sources */, @@ -3649,6 +3656,7 @@ 4A1DEF46293051C600322608 /* LoggingTests.m in Sources */, 930999521F1658F800F006A1 /* ThemeServiceRemoteTests.m in Sources */, 8BE67ED324AD05D3004DB4C9 /* Decodable+DictionaryTests.swift in Sources */, + 0CE311BF2DCBB588003AADB3 /* SubscribersServiceRemoteTests.swift in Sources */, FEE48EF62A4B3602008A48E0 /* BlogServiceRemote+ActiveFeaturesTests.swift in Sources */, 74B335DA1F06F3D60053A184 /* WordPressComRestApiTests.swift in Sources */, FA87FE0724EB39C4003FBEE3 /* ReaderPostServiceRemote+SubscriptionTests.swift in Sources */, From 5ecc1942f9bbeb0e5a18af77db3b7404c1d4c826 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 7 May 2025 11:51:16 -0400 Subject: [PATCH 08/17] Add getSubsciberStats --- .../Models/RemoteSubscriber.swift | 20 ++++++------- .../Services/SubscribersServiceRemote.swift | 28 +++++++++++++++++++ .../site-subscriber-stats-response.json | 6 ++++ .../Tests/SubscribersServiceRemoteTests.swift | 13 ++++++++- WordPressKit.xcodeproj/project.pbxproj | 4 +++ 5 files changed, 60 insertions(+), 11 deletions(-) create mode 100644 Tests/WordPressKitTests/Mock Data/site-subscriber-stats-response.json diff --git a/Sources/WordPressKit/Models/RemoteSubscriber.swift b/Sources/WordPressKit/Models/RemoteSubscriber.swift index f6414b7d..ccb1d9f3 100644 --- a/Sources/WordPressKit/Models/RemoteSubscriber.swift +++ b/Sources/WordPressKit/Models/RemoteSubscriber.swift @@ -1,23 +1,23 @@ import Foundation public struct RemoteSubscriber: Decodable { - public let userID: Int - public let subscriptionID: Int + public let subscriberID: Int + public let dotComUserID: Int + public let displayName: String? + public let avatar: String? public let emailAddress: String? public let dateSubscribed: Date - public let isEmailSubscriber: Bool + public let isEmailSubscriptionEnabled: Bool public let subscriptionStatus: String? - public let displayName: String? - public let avatar: String? private enum CodingKeys: String, CodingKey { - case userID = "user_id" - case subscriptionID = "subscription_id" + case subscriberID = "subscription_id" + case dotComUserID = "user_id" + case displayName = "display_name" case emailAddress = "email_address" + case avatar case dateSubscribed = "date_subscribed" - case isEmailSubscriber = "is_email_subscriber" + case isEmailSubscriptionEnabled = "is_email_subscriber" case subscriptionStatus = "subscription_status" - case displayName = "display_name" - case avatar } } diff --git a/Sources/WordPressKit/Services/SubscribersServiceRemote.swift b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift index 9c4d552f..3e525402 100644 --- a/Sources/WordPressKit/Services/SubscribersServiceRemote.swift +++ b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift @@ -54,6 +54,8 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { public var subscribers: [RemoteSubscriber] } + /// Gets the list of the site subscribers, including WordPress.com users and + /// email subscribers. public func getSubscribers( siteID: Int, page: Int? = nil, @@ -93,4 +95,30 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { type: GetSubscribersResponse.self ).get().body } + + public struct GetSubscriberStatsResponse: Decodable { + public var emailsSent: Int + public var uniqueOpens: Int + public var uniqueClicks: Int + } + + /// Gets stats for the given subscriber. + /// + /// Example: https://public-api.wordpress.com/wpcom/v2/sites/239619264/individual-subscriber-stats?subscription_id=907116368 + public func getSubsciberStats( + siteID: Int, + subscriberID: Int + ) async throws -> GetSubscriberStatsResponse { + let url = self.path(forEndpoint: "sites/\(siteID)/individual-subscriber-stats", withVersion: ._2_0) + let query: [String: Any] = [ + "subscription_id": 907116368 + ] + return try await wordPressComRestApi.perform( + .get, + URLString: url, + parameters: query, + jsonDecoder: JSONDecoder.apiDecoder, + type: GetSubscriberStatsResponse.self + ).get().body + } } diff --git a/Tests/WordPressKitTests/Mock Data/site-subscriber-stats-response.json b/Tests/WordPressKitTests/Mock Data/site-subscriber-stats-response.json new file mode 100644 index 00000000..fbb84824 --- /dev/null +++ b/Tests/WordPressKitTests/Mock Data/site-subscriber-stats-response.json @@ -0,0 +1,6 @@ +{ + "emails_sent": 1, + "unique_opens": 2, + "unique_clicks": 3, + "blog_registration_date": "2024-12-04 16:00:32" +} diff --git a/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift b/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift index 97c2ea05..4da74a60 100644 --- a/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift +++ b/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift @@ -14,6 +14,17 @@ class SubscribersServiceRemoteTests: RemoteTestCase, RESTTestable { XCTAssertEqual(response.total, 1) let subscriber = try XCTUnwrap(response.subscribers.first) - XCTAssertEqual(subscriber.userID, 1) + XCTAssertEqual(subscriber.dotComUserID, 1) + } + + func testDecoderSubscriberStatsResponse() throws { + let data = try JSONLoader.data(named: "site-subscriber-stats-response") + + let decoder = JSONDecoder.apiDecoder + let response = try decoder.decode(SubscribersServiceRemote.GetSubscriberStatsResponse.self, from: data) + + XCTAssertEqual(response.emailsSent, 1) + XCTAssertEqual(response.uniqueOpens, 2) + XCTAssertEqual(response.uniqueClicks, 3) } } diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index df79b6a4..dafbbefe 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -53,6 +53,7 @@ 0CCD4C622C41712800B53F9A /* wpxmlrpc in Frameworks */ = {isa = PBXBuildFile; productRef = 0CCD4C612C41712800B53F9A /* wpxmlrpc */; }; 0CE311BD2DCBB52C003AADB3 /* SubscribersServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE311BC2DCBB52C003AADB3 /* SubscribersServiceRemote.swift */; }; 0CE311BF2DCBB588003AADB3 /* SubscribersServiceRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE311BE2DCBB588003AADB3 /* SubscribersServiceRemoteTests.swift */; }; + 0CE311C52DCBB970003AADB3 /* site-subscriber-stats-response.json in Resources */ = {isa = PBXBuildFile; fileRef = 0CE311C42DCBB970003AADB3 /* site-subscriber-stats-response.json */; }; 0CE4E8252DC027AC00056DD9 /* RemoteSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE4E8242DC027AC00056DD9 /* RemoteSubscriber.swift */; }; 0CED1FE82B617CF300E6DD52 /* AtomicSiteServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CED1FE72B617CF300E6DD52 /* AtomicSiteServiceRemote.swift */; }; 0CED1FEB2B617D7D00E6DD52 /* AtomicLogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CED1FEA2B617D7D00E6DD52 /* AtomicLogs.swift */; }; @@ -831,6 +832,7 @@ 0CCD4C5B2C41700B00B53F9A /* UIDevice+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extensions.swift"; sourceTree = ""; }; 0CE311BC2DCBB52C003AADB3 /* SubscribersServiceRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribersServiceRemote.swift; sourceTree = ""; }; 0CE311BE2DCBB588003AADB3 /* SubscribersServiceRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribersServiceRemoteTests.swift; sourceTree = ""; }; + 0CE311C42DCBB970003AADB3 /* site-subscriber-stats-response.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "site-subscriber-stats-response.json"; sourceTree = ""; }; 0CE4E8242DC027AC00056DD9 /* RemoteSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSubscriber.swift; sourceTree = ""; }; 0CED1FE72B617CF300E6DD52 /* AtomicSiteServiceRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicSiteServiceRemote.swift; sourceTree = ""; }; 0CED1FEA2B617D7D00E6DD52 /* AtomicLogs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicLogs.swift; sourceTree = ""; }; @@ -2553,6 +2555,7 @@ 74D67F0C1F15C2D70010C5ED /* site-roles-auth-failure.json */, 74D67F0D1F15C2D70010C5ED /* site-roles-bad-json-failure.json */, 0C8069A62DC03E85008DFC2F /* site-subscribers-response.json */, + 0CE311C42DCBB970003AADB3 /* site-subscriber-stats-response.json */, 74D67F0E1F15C2D70010C5ED /* site-roles-success.json */, D8DB404121EF22B500B8238E /* site-segments-multiple.json */, D813437721F6D7DC0060D99A /* site-segments-single.json */, @@ -3127,6 +3130,7 @@ E1787DB0200E564B004CB3AF /* timezones.json in Resources */, 93BD275E1EE73442002BB00B /* me-bad-json-failure.json in Resources */, FFE247AF20C891E6002DF3A2 /* WordPressComOAuthWrongPasswordFail.json in Resources */, + 0CE311C52DCBB970003AADB3 /* site-subscriber-stats-response.json in Resources */, F194E1252417EE7E00874408 /* atomic-get-auth-cookie-success.json in Resources */, 731BA83A21DED358000FDFCD /* site-creation-success.json in Resources */, FEFFD99726C158F400F34231 /* share-app-content-success.json in Resources */, From e8e865267f0bf122bcac1367dac2675ab4db06a0 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 7 May 2025 12:03:07 -0400 Subject: [PATCH 09/17] Add getSubsciberDetails --- Package.swift | 4 +- .../Models/RemoteSubscriber.swift | 23 ----- .../Services/SubscribersServiceRemote.swift | 83 ++++++++++++++++++- .../site-subscriber-get-details-response.json | 15 ++++ .../Tests/SubscribersServiceRemoteTests.swift | 12 +++ WordPressKit.xcodeproj/project.pbxproj | 8 +- 6 files changed, 113 insertions(+), 32 deletions(-) delete mode 100644 Sources/WordPressKit/Models/RemoteSubscriber.swift create mode 100644 Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response.json diff --git a/Package.swift b/Package.swift index 9f842f83..70e8c781 100644 --- a/Package.swift +++ b/Package.swift @@ -11,8 +11,8 @@ let package = Package( targets: [ .binaryTarget( name: "WordPressKit", - url: "https://github.com/user-attachments/files/20067014/WordPressKit.zip", - checksum: "e20c387a1c32306e502326af03f46629140b9d1bc994de3c614890a0fd24b690" + url: "https://github.com/user-attachments/files/20087743/WordPressKit.zip", + checksum: "138689853d7a65384fa5dae5b5732b40769689a0108f4265e23cc47ca3eea647" ), ] ) diff --git a/Sources/WordPressKit/Models/RemoteSubscriber.swift b/Sources/WordPressKit/Models/RemoteSubscriber.swift deleted file mode 100644 index ccb1d9f3..00000000 --- a/Sources/WordPressKit/Models/RemoteSubscriber.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -public struct RemoteSubscriber: Decodable { - public let subscriberID: Int - public let dotComUserID: Int - public let displayName: String? - public let avatar: String? - public let emailAddress: String? - public let dateSubscribed: Date - public let isEmailSubscriptionEnabled: Bool - public let subscriptionStatus: String? - - private enum CodingKeys: String, CodingKey { - case subscriberID = "subscription_id" - case dotComUserID = "user_id" - case displayName = "display_name" - case emailAddress = "email_address" - case avatar - case dateSubscribed = "date_subscribed" - case isEmailSubscriptionEnabled = "is_email_subscriber" - case subscriptionStatus = "subscription_status" - } -} diff --git a/Sources/WordPressKit/Services/SubscribersServiceRemote.swift b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift index 3e525402..bda7f1a5 100644 --- a/Sources/WordPressKit/Services/SubscribersServiceRemote.swift +++ b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift @@ -2,7 +2,7 @@ import Foundation public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { - // MARK: GET + // MARK: GET Subscribers (Paginated List) public struct GetSubscribersParameters: Hashable { public var sortField: SortField? @@ -51,7 +51,29 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { public var total: Int public var pages: Int public var page: Int - public var subscribers: [RemoteSubscriber] + public var subscribers: [Subscriber] + + public struct Subscriber: Decodable { + public let subscriberID: Int + public let dotComUserID: Int + public let displayName: String? + public let avatar: String? + public let emailAddress: String? + public let dateSubscribed: Date + public let isEmailSubscriptionEnabled: Bool + public let subscriptionStatus: String? + + private enum CodingKeys: String, CodingKey { + case subscriberID = "subscription_id" + case dotComUserID = "user_id" + case displayName = "display_name" + case emailAddress = "email_address" + case avatar + case dateSubscribed = "date_subscribed" + case isEmailSubscriptionEnabled = "is_email_subscriber" + case subscriptionStatus = "subscription_status" + } + } } /// Gets the list of the site subscribers, including WordPress.com users and @@ -96,6 +118,61 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { ).get().body } + // MARK: GET Subscriber (Individual Details) + + public struct GetSubscriberDetailsResponse: Decodable { + public let subscriberID: Int + public let dotComUserID: Int + public let displayName: String? + public let avatar: String? + public let emailAddress: String? + public let dateSubscribed: Date + public let isEmailSubscriptionEnabled: Bool + public let subscriptionStatus: String? + public let country: Country? + + public struct Country: Decodable { + public var code: String? + public var name: String? + } + + private enum CodingKeys: String, CodingKey { + case subscriberID = "subscription_id" + case dotComUserID = "user_id" + case displayName = "display_name" + case emailAddress = "email_address" + case avatar + case dateSubscribed = "date_subscribed" + case isEmailSubscriptionEnabled = "is_email_subscriber" + case subscriptionStatus = "subscription_status" + case country + } + } + + /// Gets stats for the given subscriber. + /// + /// Example: https://public-api.wordpress.com/wpcom/v2/sites/239619264/subscribers/individual?subscription_id=907116368 + public func getSubsciberDetails( + siteID: Int, + subscriberID: Int + ) async throws -> GetSubscriberDetailsResponse { + let url = self.path(forEndpoint: "sites/\(siteID)/subscribers/individual", withVersion: ._2_0) + let query: [String: Any] = [ + "subscription_id": subscriberID + ] + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.supportMultipleDateFormats + + return try await wordPressComRestApi.perform( + .get, + URLString: url, + parameters: query, + jsonDecoder: decoder, + type: GetSubscriberDetailsResponse.self + ).get().body + } + public struct GetSubscriberStatsResponse: Decodable { public var emailsSent: Int public var uniqueOpens: Int @@ -111,7 +188,7 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { ) async throws -> GetSubscriberStatsResponse { let url = self.path(forEndpoint: "sites/\(siteID)/individual-subscriber-stats", withVersion: ._2_0) let query: [String: Any] = [ - "subscription_id": 907116368 + "subscription_id": subscriberID ] return try await wordPressComRestApi.perform( .get, diff --git a/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response.json b/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response.json new file mode 100644 index 00000000..41aae359 --- /dev/null +++ b/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response.json @@ -0,0 +1,15 @@ +{ + "user_id": 255064965, + "subscription_id": 907116368, + "email_address": "grebenyuk.alexander+test@icloud.com", + "date_subscribed": "2025-04-17T14:40:00+00:00", + "is_email_subscriber": false, + "subscription_status": "Subscribed", + "avatar": "https://0.gravatar.com/avatar/694664524f7d391c4425ab07627f4e44e970f597985d24ce3dc4c27173316c20?s=128&d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D128&r=G", + "display_name": "Alex", + "url": "http://test841027.wordpress.com", + "country": { + "code": "US", + "name": "United States" + } +} diff --git a/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift b/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift index 4da74a60..3cb7ead3 100644 --- a/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift +++ b/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift @@ -17,6 +17,18 @@ class SubscribersServiceRemoteTests: RemoteTestCase, RESTTestable { XCTAssertEqual(subscriber.dotComUserID, 1) } + func testDecoderSubscriberDetailsResponse() throws { + let data = try JSONLoader.data(named: "site-subscriber-get-details-response") + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.supportMultipleDateFormats + + let response = try decoder.decode(SubscribersServiceRemote.GetSubscriberDetailsResponse.self, from: data) + + XCTAssertEqual(response.country?.code, "US") + XCTAssertEqual(response.country?.name, "United States") + } + func testDecoderSubscriberStatsResponse() throws { let data = try JSONLoader.data(named: "site-subscriber-stats-response") diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index dafbbefe..ac12dead 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -54,7 +54,7 @@ 0CE311BD2DCBB52C003AADB3 /* SubscribersServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE311BC2DCBB52C003AADB3 /* SubscribersServiceRemote.swift */; }; 0CE311BF2DCBB588003AADB3 /* SubscribersServiceRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE311BE2DCBB588003AADB3 /* SubscribersServiceRemoteTests.swift */; }; 0CE311C52DCBB970003AADB3 /* site-subscriber-stats-response.json in Resources */ = {isa = PBXBuildFile; fileRef = 0CE311C42DCBB970003AADB3 /* site-subscriber-stats-response.json */; }; - 0CE4E8252DC027AC00056DD9 /* RemoteSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE4E8242DC027AC00056DD9 /* RemoteSubscriber.swift */; }; + 0CE311C72DCBBA01003AADB3 /* site-subscriber-get-details-response.json in Resources */ = {isa = PBXBuildFile; fileRef = 0CE311C62DCBBA01003AADB3 /* site-subscriber-get-details-response.json */; }; 0CED1FE82B617CF300E6DD52 /* AtomicSiteServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CED1FE72B617CF300E6DD52 /* AtomicSiteServiceRemote.swift */; }; 0CED1FEB2B617D7D00E6DD52 /* AtomicLogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CED1FEA2B617D7D00E6DD52 /* AtomicLogs.swift */; }; 1769DEAA24729AFF00F42EFC /* HomepageSettingsServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769DEA924729AFF00F42EFC /* HomepageSettingsServiceRemote.swift */; }; @@ -833,7 +833,7 @@ 0CE311BC2DCBB52C003AADB3 /* SubscribersServiceRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribersServiceRemote.swift; sourceTree = ""; }; 0CE311BE2DCBB588003AADB3 /* SubscribersServiceRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribersServiceRemoteTests.swift; sourceTree = ""; }; 0CE311C42DCBB970003AADB3 /* site-subscriber-stats-response.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "site-subscriber-stats-response.json"; sourceTree = ""; }; - 0CE4E8242DC027AC00056DD9 /* RemoteSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSubscriber.swift; sourceTree = ""; }; + 0CE311C62DCBBA01003AADB3 /* site-subscriber-get-details-response.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "site-subscriber-get-details-response.json"; sourceTree = ""; }; 0CED1FE72B617CF300E6DD52 /* AtomicSiteServiceRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicSiteServiceRemote.swift; sourceTree = ""; }; 0CED1FEA2B617D7D00E6DD52 /* AtomicLogs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicLogs.swift; sourceTree = ""; }; 1769DEA924729AFF00F42EFC /* HomepageSettingsServiceRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomepageSettingsServiceRemote.swift; sourceTree = ""; }; @@ -1876,7 +1876,6 @@ 4A68E3DC294070A7004AC3DC /* RemoteReaderSite.swift */, 4A68E3E0294076C1004AC3DC /* RemoteReaderSiteInfo.swift */, 9F3E0B9A208732B2009CB5BA /* RemoteReaderSiteInfoSubscription.swift */, - 0CE4E8242DC027AC00056DD9 /* RemoteSubscriber.swift */, 4A68E3DE29407100004AC3DC /* RemoteReaderTopic.swift */, 74E2295D1F1E777B0085F7F2 /* RemoteSharingButton.swift */, 7430C9C81F192F260051B8E6 /* RemoteSourcePostAttribution.h */, @@ -2556,6 +2555,7 @@ 74D67F0D1F15C2D70010C5ED /* site-roles-bad-json-failure.json */, 0C8069A62DC03E85008DFC2F /* site-subscribers-response.json */, 0CE311C42DCBB970003AADB3 /* site-subscriber-stats-response.json */, + 0CE311C62DCBBA01003AADB3 /* site-subscriber-get-details-response.json */, 74D67F0E1F15C2D70010C5ED /* site-roles-success.json */, D8DB404121EF22B500B8238E /* site-segments-multiple.json */, D813437721F6D7DC0060D99A /* site-segments-single.json */, @@ -3157,6 +3157,7 @@ B04D8C052BB7895A002717A2 /* stats-insight-followers.json in Resources */, 74D67F3B1F15C3740010C5ED /* site-viewers-delete-success.json in Resources */, 93BD275F1EE73442002BB00B /* me-sites-auth-failure.json in Resources */, + 0CE311C72DCBBA01003AADB3 /* site-subscriber-get-details-response.json in Resources */, 74585BA11F0D6F5300E7E667 /* domain-service-empty.json in Resources */, 74C473C51EF33242009918F2 /* site-active-purchases-two-active-success.json in Resources */, 4A3239682B74319400EFD2A8 /* self-hosted-plugins-install.json in Resources */, @@ -3509,7 +3510,6 @@ 3FD634F32BC3AD6200CEDF5E /* Result+Callback.swift in Sources */, B5A4822E20AC6C1A009D95F6 /* WPKitLogging.m in Sources */, 3FE2E97C2BC3A332002CA2E1 /* WordPressComRestApi.swift in Sources */, - 0CE4E8252DC027AC00056DD9 /* RemoteSubscriber.swift in Sources */, FE6C673C2BB739950083ECAB /* NSAttributedString+extensions.swift in Sources */, 7430C9A61F1927180051B8E6 /* ReaderSiteServiceRemote.m in Sources */, FEE4EF57272FDD4B003CDA3C /* RemoteCommentV2.swift in Sources */, From a128446199ff6715ef6876207600b17716df6fe3 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 8 May 2025 11:37:15 -0400 Subject: [PATCH 10/17] Add support for plans --- Package.swift | 4 +-- .../Services/SubscribersServiceRemote.swift | 32 +++++++++++++++++++ .../site-subscriber-get-details-response.json | 30 ++++++++++++++++- .../Tests/SubscribersServiceRemoteTests.swift | 5 +++ 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 70e8c781..308af351 100644 --- a/Package.swift +++ b/Package.swift @@ -11,8 +11,8 @@ let package = Package( targets: [ .binaryTarget( name: "WordPressKit", - url: "https://github.com/user-attachments/files/20087743/WordPressKit.zip", - checksum: "138689853d7a65384fa5dae5b5732b40769689a0108f4265e23cc47ca3eea647" + url: "https://github.com/user-attachments/files/20105676/WordPressKit.zip", + checksum: "6a446e44dda98d3f5d0d916fbd946d1bf602dfb6124e4ce01aeb7a0c161ee3f6" ), ] ) diff --git a/Sources/WordPressKit/Services/SubscribersServiceRemote.swift b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift index bda7f1a5..a1824917 100644 --- a/Sources/WordPressKit/Services/SubscribersServiceRemote.swift +++ b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift @@ -126,26 +126,58 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { public let displayName: String? public let avatar: String? public let emailAddress: String? + public let siteURL: String? public let dateSubscribed: Date public let isEmailSubscriptionEnabled: Bool public let subscriptionStatus: String? public let country: Country? + public var plans: [Plan]? public struct Country: Decodable { public var code: String? public var name: String? } + public struct Plan: Decodable { + public let isGift: Bool + public let giftId: Int? + public let paidSubscriptionId: String? + public let status: String + public let title: String + public let currency: String? + public let renewInterval: String? + public let inactiveRenewInterval: String? + public let renewalPrice: Decimal + public let startDate: Date + public let endDate: Date + + enum CodingKeys: String, CodingKey { + case isGift = "is_gift" + case giftId = "gift_id" + case paidSubscriptionId = "paid_subscription_id" + case status + case title + case currency + case renewInterval = "renew_interval" + case inactiveRenewInterval = "inactive_renew_interval" + case renewalPrice = "renewal_price" + case startDate = "start_date" + case endDate = "end_date" + } + } + private enum CodingKeys: String, CodingKey { case subscriberID = "subscription_id" case dotComUserID = "user_id" case displayName = "display_name" case emailAddress = "email_address" case avatar + case siteURL = "url" case dateSubscribed = "date_subscribed" case isEmailSubscriptionEnabled = "is_email_subscriber" case subscriptionStatus = "subscription_status" case country + case plans } } diff --git a/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response.json b/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response.json index 41aae359..d5b9c1b0 100644 --- a/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response.json +++ b/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response.json @@ -11,5 +11,33 @@ "country": { "code": "US", "name": "United States" - } + }, + "plans": [ + { + "is_gift": false, + "gift_id": null, + "paid_subscription_id": "12422686", + "status": "active", + "title": "Newsletter Tier", + "currency": "USD", + "renew_interval": "1 month", + "inactive_renew_interval": null, + "renewal_price": 0.5, + "start_date": "2025-01-13T18:51:55+00:00", + "end_date": "2025-02-13T18:51:55+00:00" + }, + { + "is_gift": true, + "gift_id": 31, + "paid_subscription_id": null, + "status": "active", + "title": "Newsletter Tier 3", + "currency": "USD", + "renew_interval": "one-time", + "inactive_renew_interval": null, + "renewal_price": 0, + "start_date": "2025-05-08T14:50:28+00:00", + "end_date": "2025-06-07T14:50:28+00:00" + } + ] } diff --git a/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift b/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift index 3cb7ead3..a3eb912b 100644 --- a/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift +++ b/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift @@ -27,6 +27,11 @@ class SubscribersServiceRemoteTests: RemoteTestCase, RESTTestable { XCTAssertEqual(response.country?.code, "US") XCTAssertEqual(response.country?.name, "United States") + + let plan = try XCTUnwrap(response.plans?.first) + XCTAssertFalse(plan.isGift) + XCTAssertEqual(plan.status, "active") + XCTAssertEqual(plan.paidSubscriptionId, "12422686") } func testDecoderSubscriberStatsResponse() throws { From 34f8857dbd9200c58a562c6ee32ead8f99e7e07d Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 8 May 2025 12:58:39 -0400 Subject: [PATCH 11/17] Add SubsciberBasicInfoResponse --- Package.swift | 4 ++-- .../Services/SubscribersServiceRemote.swift | 23 +++++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 308af351..1fd5e9d8 100644 --- a/Package.swift +++ b/Package.swift @@ -11,8 +11,8 @@ let package = Package( targets: [ .binaryTarget( name: "WordPressKit", - url: "https://github.com/user-attachments/files/20105676/WordPressKit.zip", - checksum: "6a446e44dda98d3f5d0d916fbd946d1bf602dfb6124e4ce01aeb7a0c161ee3f6" + url: "https://github.com/user-attachments/files/20106710/WordPressKit.zip", + checksum: "718a32f677c5ce49bd69f7cb0c8605993370f423aa8f088deab99a6f40dc45ac" ), ] ) diff --git a/Sources/WordPressKit/Services/SubscribersServiceRemote.swift b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift index a1824917..eb7f0476 100644 --- a/Sources/WordPressKit/Services/SubscribersServiceRemote.swift +++ b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift @@ -53,7 +53,7 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { public var page: Int public var subscribers: [Subscriber] - public struct Subscriber: Decodable { + public struct Subscriber: Decodable, SubsciberBasicInfoResponse { public let subscriberID: Int public let dotComUserID: Int public let displayName: String? @@ -120,7 +120,16 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { // MARK: GET Subscriber (Individual Details) - public struct GetSubscriberDetailsResponse: Decodable { + public protocol SubsciberBasicInfoResponse { + var dotComUserID: Int { get } + var subscriberID: Int { get } + var displayName: String? { get } + var emailAddress: String? { get } + var avatar: String? { get } + var dateSubscribed: Date { get } + } + + public struct GetSubscriberDetailsResponse: Decodable, SubsciberBasicInfoResponse { public let subscriberID: Int public let dotComUserID: Int public let displayName: String? @@ -231,3 +240,13 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { ).get().body } } + +extension SubscribersServiceRemote.SubsciberBasicInfoResponse { + public var avatarURL: URL? { + avatar.flatMap(URL.init) + } + + public var isDotComUser: Bool { + dotComUserID > 0 + } +} From f20b9561e9aeca53042e471b15e0d4aea8d162a1 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 9 May 2025 10:34:32 -0400 Subject: [PATCH 12/17] Add type parameter --- Package.swift | 4 ++-- .../Services/SubscribersServiceRemote.swift | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 1fd5e9d8..179790fa 100644 --- a/Package.swift +++ b/Package.swift @@ -11,8 +11,8 @@ let package = Package( targets: [ .binaryTarget( name: "WordPressKit", - url: "https://github.com/user-attachments/files/20106710/WordPressKit.zip", - checksum: "718a32f677c5ce49bd69f7cb0c8605993370f423aa8f088deab99a6f40dc45ac" + url: "https://github.com/user-attachments/files/20123946/WordPressKit.zip", + checksum: "e7905c7d063682c3a3433b4b36578169081c74895db02ec55ec8de2745c799ef" ), ] ) diff --git a/Sources/WordPressKit/Services/SubscribersServiceRemote.swift b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift index eb7f0476..e6513f67 100644 --- a/Sources/WordPressKit/Services/SubscribersServiceRemote.swift +++ b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift @@ -195,11 +195,13 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { /// Example: https://public-api.wordpress.com/wpcom/v2/sites/239619264/subscribers/individual?subscription_id=907116368 public func getSubsciberDetails( siteID: Int, - subscriberID: Int + subscriberID: Int, + type: String = "email" ) async throws -> GetSubscriberDetailsResponse { let url = self.path(forEndpoint: "sites/\(siteID)/subscribers/individual", withVersion: ._2_0) let query: [String: Any] = [ - "subscription_id": subscriberID + "subscription_id": subscriberID, + "type": type ] let decoder = JSONDecoder() @@ -239,6 +241,12 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { type: GetSubscriberStatsResponse.self ).get().body } + + // MARK: POST Delete Subscriber + + public func deleteSubscriber() { + + } } extension SubscribersServiceRemote.SubsciberBasicInfoResponse { From 0454d46f6a54ad48c6f9af0745658140852c79ac Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 9 May 2025 11:18:03 -0400 Subject: [PATCH 13/17] Safer decoding --- Package.swift | 4 +- .../Services/SubscribersServiceRemote.swift | 79 +++++++++---------- .../Utility/StringCodingKey.swift | 27 +++++++ ...-get-details-response-invalid-country.json | 15 ++++ .../Tests/SubscribersServiceRemoteTests.swift | 11 +++ WordPressKit.xcodeproj/project.pbxproj | 8 ++ 6 files changed, 101 insertions(+), 43 deletions(-) create mode 100644 Sources/WordPressKit/Utility/StringCodingKey.swift create mode 100644 Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response-invalid-country.json diff --git a/Package.swift b/Package.swift index 179790fa..27c0046b 100644 --- a/Package.swift +++ b/Package.swift @@ -11,8 +11,8 @@ let package = Package( targets: [ .binaryTarget( name: "WordPressKit", - url: "https://github.com/user-attachments/files/20123946/WordPressKit.zip", - checksum: "e7905c7d063682c3a3433b4b36578169081c74895db02ec55ec8de2745c799ef" + url: "https://github.com/user-attachments/files/20124623/WordPressKit.zip", + checksum: "fb3d6043a07ffe1ba50bbbf3ca8daaa001bff7f05273bad2a113c89705400b1b" ), ] ) diff --git a/Sources/WordPressKit/Services/SubscribersServiceRemote.swift b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift index e6513f67..778826e7 100644 --- a/Sources/WordPressKit/Services/SubscribersServiceRemote.swift +++ b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift @@ -63,15 +63,16 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { public let isEmailSubscriptionEnabled: Bool public let subscriptionStatus: String? - private enum CodingKeys: String, CodingKey { - case subscriberID = "subscription_id" - case dotComUserID = "user_id" - case displayName = "display_name" - case emailAddress = "email_address" - case avatar - case dateSubscribed = "date_subscribed" - case isEmailSubscriptionEnabled = "is_email_subscriber" - case subscriptionStatus = "subscription_status" + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + subscriberID = try container.decode(Int.self, forKey: "subscription_id") + dotComUserID = try container.decode(Int.self, forKey: "user_id") + displayName = try? container.decodeIfPresent(String.self, forKey: "display_name") + avatar = try? container.decodeIfPresent(String.self, forKey: "avatar") + emailAddress = try? container.decodeIfPresent(String.self, forKey: "email_address") + dateSubscribed = try container.decode(Date.self, forKey: "date_subscribed") + isEmailSubscriptionEnabled = try container.decode(Bool.self, forKey: "is_email_subscriber") + subscriptionStatus = try? container.decodeIfPresent(String.self, forKey: "subscription_status") } } } @@ -129,7 +130,7 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { var dateSubscribed: Date { get } } - public struct GetSubscriberDetailsResponse: Decodable, SubsciberBasicInfoResponse { + public final class GetSubscriberDetailsResponse: Decodable, SubsciberBasicInfoResponse { public let subscriberID: Int public let dotComUserID: Int public let displayName: String? @@ -140,7 +141,7 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { public let isEmailSubscriptionEnabled: Bool public let subscriptionStatus: String? public let country: Country? - public var plans: [Plan]? + public let plans: [Plan]? public struct Country: Decodable { public var code: String? @@ -160,33 +161,35 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { public let startDate: Date public let endDate: Date - enum CodingKeys: String, CodingKey { - case isGift = "is_gift" - case giftId = "gift_id" - case paidSubscriptionId = "paid_subscription_id" - case status - case title - case currency - case renewInterval = "renew_interval" - case inactiveRenewInterval = "inactive_renew_interval" - case renewalPrice = "renewal_price" - case startDate = "start_date" - case endDate = "end_date" + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + isGift = try container.decode(Bool.self, forKey: "is_gift") + giftId = try container.decodeIfPresent(Int.self, forKey: "gift_id") + paidSubscriptionId = try container.decodeIfPresent(String.self, forKey: "paid_subscription_id") + status = try container.decode(String.self, forKey: "status") + title = try container.decode(String.self, forKey: "title") + currency = try container.decodeIfPresent(String.self, forKey: "currency") + renewInterval = try? container.decodeIfPresent(String.self, forKey: "renew_interval") + inactiveRenewInterval = try? container.decodeIfPresent(String.self, forKey: "inactive_renew_interval") + renewalPrice = try container.decode(Decimal.self, forKey: "renewal_price") + startDate = try container.decode(Date.self, forKey: "start_date") + endDate = try container.decode(Date.self, forKey: "end_date") } } - private enum CodingKeys: String, CodingKey { - case subscriberID = "subscription_id" - case dotComUserID = "user_id" - case displayName = "display_name" - case emailAddress = "email_address" - case avatar - case siteURL = "url" - case dateSubscribed = "date_subscribed" - case isEmailSubscriptionEnabled = "is_email_subscriber" - case subscriptionStatus = "subscription_status" - case country - case plans + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + subscriberID = try container.decode(Int.self, forKey: "subscription_id") + dotComUserID = try container.decode(Int.self, forKey: "user_id") + displayName = try? container.decodeIfPresent(String.self, forKey: "display_name") + avatar = try? container.decodeIfPresent(String.self, forKey: "avatar") + emailAddress = try? container.decodeIfPresent(String.self, forKey: "email_address") + siteURL = try? container.decodeIfPresent(String.self, forKey: "url") + dateSubscribed = try container.decode(Date.self, forKey: "date_subscribed") + isEmailSubscriptionEnabled = try container.decode(Bool.self, forKey: "is_email_subscriber") + subscriptionStatus = try? container.decodeIfPresent(String.self, forKey: "subscription_status") + country = try? container.decodeIfPresent(Country.self, forKey: "country") + plans = try container.decodeIfPresent([Plan].self, forKey: "plans") } } @@ -241,12 +244,6 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { type: GetSubscriberStatsResponse.self ).get().body } - - // MARK: POST Delete Subscriber - - public func deleteSubscriber() { - - } } extension SubscribersServiceRemote.SubsciberBasicInfoResponse { diff --git a/Sources/WordPressKit/Utility/StringCodingKey.swift b/Sources/WordPressKit/Utility/StringCodingKey.swift new file mode 100644 index 00000000..9f4c2bb1 --- /dev/null +++ b/Sources/WordPressKit/Utility/StringCodingKey.swift @@ -0,0 +1,27 @@ +import Foundation + +struct StringCodingKey: CodingKey, ExpressibleByStringLiteral { + private let string: String + private var int: Int? + + var stringValue: String { return string } + + init(string: String) { + self.string = string + } + + init?(stringValue: String) { + self.string = stringValue + } + + var intValue: Int? { return int } + + init?(intValue: Int) { + self.string = String(describing: intValue) + self.int = intValue + } + + init(stringLiteral value: String) { + self.string = value + } +} diff --git a/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response-invalid-country.json b/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response-invalid-country.json new file mode 100644 index 00000000..80542b3d --- /dev/null +++ b/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response-invalid-country.json @@ -0,0 +1,15 @@ +{ + "user_id": 255064965, + "subscription_id": 907116368, + "email_address": "grebenyuk.alexander+test@icloud.com", + "date_subscribed": "2025-04-17T14:40:00+00:00", + "is_email_subscriber": false, + "subscription_status": "Subscribed", + "avatar": "https://0.gravatar.com/avatar/694664524f7d391c4425ab07627f4e44e970f597985d24ce3dc4c27173316c20?s=128&d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D128&r=G", + "display_name": "Alex", + "url": "http://test841027.wordpress.com", + "country": { + "code": "", + "name": false + } +} diff --git a/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift b/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift index a3eb912b..5e6f4bbd 100644 --- a/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift +++ b/Tests/WordPressKitTests/Tests/SubscribersServiceRemoteTests.swift @@ -34,6 +34,17 @@ class SubscribersServiceRemoteTests: RemoteTestCase, RESTTestable { XCTAssertEqual(plan.paidSubscriptionId, "12422686") } + func testDecoderSubscriberDetailsInvalidCountry() throws { + let data = try JSONLoader.data(named: "site-subscriber-get-details-response-invalid-country") + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.supportMultipleDateFormats + + let response = try decoder.decode(SubscribersServiceRemote.GetSubscriberDetailsResponse.self, from: data) + + XCTAssertNil(response.country) + } + func testDecoderSubscriberStatsResponse() throws { let data = try JSONLoader.data(named: "site-subscriber-stats-response") diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index ac12dead..c6177f16 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -51,6 +51,8 @@ 0CCD4C5C2C41700B00B53F9A /* UIDevice+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCD4C5B2C41700B00B53F9A /* UIDevice+Extensions.swift */; }; 0CCD4C5F2C41711800B53F9A /* NSObject-SafeExpectations in Frameworks */ = {isa = PBXBuildFile; productRef = 0CCD4C5E2C41711800B53F9A /* NSObject-SafeExpectations */; }; 0CCD4C622C41712800B53F9A /* wpxmlrpc in Frameworks */ = {isa = PBXBuildFile; productRef = 0CCD4C612C41712800B53F9A /* wpxmlrpc */; }; + 0CD5D3DD2DCE4F5500B4E679 /* StringCodingKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5D3DC2DCE4F5500B4E679 /* StringCodingKey.swift */; }; + 0CD5D3DF2DCE50D900B4E679 /* site-subscriber-get-details-response-invalid-country.json in Resources */ = {isa = PBXBuildFile; fileRef = 0CD5D3DE2DCE50D900B4E679 /* site-subscriber-get-details-response-invalid-country.json */; }; 0CE311BD2DCBB52C003AADB3 /* SubscribersServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE311BC2DCBB52C003AADB3 /* SubscribersServiceRemote.swift */; }; 0CE311BF2DCBB588003AADB3 /* SubscribersServiceRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE311BE2DCBB588003AADB3 /* SubscribersServiceRemoteTests.swift */; }; 0CE311C52DCBB970003AADB3 /* site-subscriber-stats-response.json in Resources */ = {isa = PBXBuildFile; fileRef = 0CE311C42DCBB970003AADB3 /* site-subscriber-stats-response.json */; }; @@ -830,6 +832,8 @@ 0CB1905F2A2A6943004D3E80 /* blaze-campaigns-search.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blaze-campaigns-search.json"; sourceTree = ""; }; 0CB190642A2A7569004D3E80 /* BlazeCampaignsSearchResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignsSearchResponse.swift; sourceTree = ""; }; 0CCD4C5B2C41700B00B53F9A /* UIDevice+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extensions.swift"; sourceTree = ""; }; + 0CD5D3DC2DCE4F5500B4E679 /* StringCodingKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringCodingKey.swift; sourceTree = ""; }; + 0CD5D3DE2DCE50D900B4E679 /* site-subscriber-get-details-response-invalid-country.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "site-subscriber-get-details-response-invalid-country.json"; sourceTree = ""; }; 0CE311BC2DCBB52C003AADB3 /* SubscribersServiceRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribersServiceRemote.swift; sourceTree = ""; }; 0CE311BE2DCBB588003AADB3 /* SubscribersServiceRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribersServiceRemoteTests.swift; sourceTree = ""; }; 0CE311C42DCBB970003AADB3 /* site-subscriber-stats-response.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "site-subscriber-stats-response.json"; sourceTree = ""; }; @@ -2037,6 +2041,7 @@ 3F3195AC266FF94B00397EE7 /* ZendeskMetadata.swift */, 4AE278432B2FAF6200E4D9B1 /* HTTPProtocolHelpers.swift */, 0CCD4C5B2C41700B00B53F9A /* UIDevice+Extensions.swift */, + 0CD5D3DC2DCE4F5500B4E679 /* StringCodingKey.swift */, ); path = Utility; sourceTree = ""; @@ -2556,6 +2561,7 @@ 0C8069A62DC03E85008DFC2F /* site-subscribers-response.json */, 0CE311C42DCBB970003AADB3 /* site-subscriber-stats-response.json */, 0CE311C62DCBBA01003AADB3 /* site-subscriber-get-details-response.json */, + 0CD5D3DE2DCE50D900B4E679 /* site-subscriber-get-details-response-invalid-country.json */, 74D67F0E1F15C2D70010C5ED /* site-roles-success.json */, D8DB404121EF22B500B8238E /* site-segments-multiple.json */, D813437721F6D7DC0060D99A /* site-segments-single.json */, @@ -3082,6 +3088,7 @@ 74C473CD1EF336BD009918F2 /* site-active-purchases-bad-json-failure.json in Resources */, 436D5645211B801100CEAA33 /* validate-domain-contact-information-response-success.json in Resources */, FE5096652A309DEE00DDD071 /* jetpack-social-with-publicize.json in Resources */, + 0CD5D3DF2DCE50D900B4E679 /* site-subscriber-get-details-response-invalid-country.json in Resources */, 74D67F351F15C3740010C5ED /* site-users-delete-not-member-failure.json in Resources */, E1E89C681FD6B2E9006E7A33 /* plugin-directory-jetpack.json in Resources */, 74D67F201F15C3240010C5ED /* people-validate-invitation-failure.json in Resources */, @@ -3471,6 +3478,7 @@ C7A09A52284104DB003096ED /* QRLoginServiceRemote.swift in Sources */, 4A68E3DD294070A7004AC3DC /* RemoteReaderSite.swift in Sources */, 40AB1ADA200FED25009B533D /* PluginDirectoryFeedPage.swift in Sources */, + 0CD5D3DD2DCE4F5500B4E679 /* StringCodingKey.swift in Sources */, 436D56352118D85800CEAA33 /* WPCountry.swift in Sources */, 74A44DCB1F13C533006CD8F4 /* NotificationSettingsServiceRemote.swift in Sources */, FAD1344525908F5F00A8FEB1 /* JetpackBackupServiceRemote.swift in Sources */, From 9b22001f2c8dfc15cd91f01eae0d3a059c0c3050 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 9 May 2025 11:19:07 -0400 Subject: [PATCH 14/17] Cleanup the test data --- ...ubscriber-get-details-response-invalid-country.json | 10 +++++----- .../site-subscriber-get-details-response.json | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response-invalid-country.json b/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response-invalid-country.json index 80542b3d..3b92ec64 100644 --- a/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response-invalid-country.json +++ b/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response-invalid-country.json @@ -1,13 +1,13 @@ { - "user_id": 255064965, - "subscription_id": 907116368, - "email_address": "grebenyuk.alexander+test@icloud.com", + "user_id": 123, + "subscription_id": 123, + "email_address": "test@example.com", "date_subscribed": "2025-04-17T14:40:00+00:00", "is_email_subscriber": false, "subscription_status": "Subscribed", - "avatar": "https://0.gravatar.com/avatar/694664524f7d391c4425ab07627f4e44e970f597985d24ce3dc4c27173316c20?s=128&d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D128&r=G", + "avatar": "https://example.com/avatar", "display_name": "Alex", - "url": "http://test841027.wordpress.com", + "url": "http://example.wordpress.com", "country": { "code": "", "name": false diff --git a/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response.json b/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response.json index d5b9c1b0..b7d637e7 100644 --- a/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response.json +++ b/Tests/WordPressKitTests/Mock Data/site-subscriber-get-details-response.json @@ -1,13 +1,13 @@ { - "user_id": 255064965, - "subscription_id": 907116368, - "email_address": "grebenyuk.alexander+test@icloud.com", + "user_id": 123, + "subscription_id": 123, + "email_address": "test@example.com", "date_subscribed": "2025-04-17T14:40:00+00:00", "is_email_subscriber": false, "subscription_status": "Subscribed", - "avatar": "https://0.gravatar.com/avatar/694664524f7d391c4425ab07627f4e44e970f597985d24ce3dc4c27173316c20?s=128&d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D128&r=G", + "avatar": "https://example.com/avatar", "display_name": "Alex", - "url": "http://test841027.wordpress.com", + "url": "http://example.wordpress.com", "country": { "code": "US", "name": "United States" From c0ccd265900cea2467bce86aac92acf682dc7123 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 9 May 2025 15:09:02 -0400 Subject: [PATCH 15/17] Make filter public --- Package.swift | 4 ++-- Sources/WordPressKit/Services/SubscribersServiceRemote.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 27c0046b..608697ea 100644 --- a/Package.swift +++ b/Package.swift @@ -11,8 +11,8 @@ let package = Package( targets: [ .binaryTarget( name: "WordPressKit", - url: "https://github.com/user-attachments/files/20124623/WordPressKit.zip", - checksum: "fb3d6043a07ffe1ba50bbbf3ca8daaa001bff7f05273bad2a113c89705400b1b" + url: "https://github.com/user-attachments/files/20127687/WordPressKit.zip", + checksum: "bbc81f893eb080a176d018f53d85e6747e529799309c0245c9e204053e75e138" ), ] ) diff --git a/Sources/WordPressKit/Services/SubscribersServiceRemote.swift b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift index 778826e7..e89f0e9e 100644 --- a/Sources/WordPressKit/Services/SubscribersServiceRemote.swift +++ b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift @@ -35,7 +35,7 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { case paid } - var filters: [String] { + public var filters: [String] { [subscriptionTypeFilter?.rawValue, paymentTypeFilter?.rawValue].compactMap { $0 } } From dfd187739b114fd1d32cbfc8c4cabaa906aa87ac Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 12 May 2025 15:49:07 -0400 Subject: [PATCH 16/17] Add importSubscribers --- Package.swift | 2 +- .../Services/SubscribersServiceRemote.swift | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 608697ea..3cc289be 100644 --- a/Package.swift +++ b/Package.swift @@ -12,7 +12,7 @@ let package = Package( .binaryTarget( name: "WordPressKit", url: "https://github.com/user-attachments/files/20127687/WordPressKit.zip", - checksum: "bbc81f893eb080a176d018f53d85e6747e529799309c0245c9e204053e75e138" + checksum: "13aa0e5952616a2f01a0f0db370ee7925d58253c2aab6e216671e8a013ab471b" ), ] ) diff --git a/Sources/WordPressKit/Services/SubscribersServiceRemote.swift b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift index e89f0e9e..b8c6770a 100644 --- a/Sources/WordPressKit/Services/SubscribersServiceRemote.swift +++ b/Sources/WordPressKit/Services/SubscribersServiceRemote.swift @@ -244,6 +244,35 @@ public class SubscribersServiceRemote: ServiceRemoteWordPressComREST { type: GetSubscriberStatsResponse.self ).get().body } + + // MARK: POST Import Subscribers + + /// Example: URL: https://public-api.wordpress.com/wpcom/v2/sites/216878809/subscribers/import?_envelope=1 + @discardableResult + public func importSubscribers( + siteID: Int, + emails: [String] + ) async throws -> ImportSubscribersResponse { + let url = self.path(forEndpoint: "sites/\(siteID)/subscribers/import", withVersion: ._2_0) + let parameters: [String: Any] = [ + "emails": emails, + "parse_only": false + ] + return try await wordPressComRestApi.perform( + .post, + URLString: url, + parameters: parameters, + type: ImportSubscribersResponse.self + ).get().body + } + + public struct ImportSubscribersResponse: Decodable { + public let uploadID: Int + + enum CodingKeys: String, CodingKey { + case uploadID = "upload_id" + } + } } extension SubscribersServiceRemote.SubsciberBasicInfoResponse { From cc7fd8a7ea609fc139e7b9d9f53b12c51002ddf4 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 12 May 2025 15:54:06 -0400 Subject: [PATCH 17/17] Fix download link --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 3cc289be..dcc9c7af 100644 --- a/Package.swift +++ b/Package.swift @@ -11,7 +11,7 @@ let package = Package( targets: [ .binaryTarget( name: "WordPressKit", - url: "https://github.com/user-attachments/files/20127687/WordPressKit.zip", + url: "https://github.com/user-attachments/files/20175119/WordPressKit.zip", checksum: "13aa0e5952616a2f01a0f0db370ee7925d58253c2aab6e216671e8a013ab471b" ), ]