From e4dbf66b8c41ffa38afd035c74bbc973d0502bcb Mon Sep 17 00:00:00 2001 From: Huong Do Date: Mon, 29 Aug 2022 11:09:10 +0700 Subject: [PATCH 001/256] Add WordPressKit to Networking --- Podfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Podfile b/Podfile index e7125eceec7..a493f78be34 100644 --- a/Podfile +++ b/Podfile @@ -145,6 +145,10 @@ def networking_pods # Used for HTML parsing aztec + + # To allow pod to pick up beta versions use -beta. E.g., 1.1.7-beta.1 + pod 'WordPressKit', '~> 4.49.0' + # pod 'WordPressKit', :git => 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', :branch => '' end # Networking Target: From 0d95833c2d5e81c5cbb6b51c23bd6c20c3a2c2ff Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 30 Aug 2022 10:13:59 +0700 Subject: [PATCH 002/256] Update podfile.lock --- Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Podfile.lock b/Podfile.lock index fd0fb269c13..e116d2b71a7 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -179,6 +179,6 @@ SPEC CHECKSUMS: ZendeskSupportProvidersSDK: 2bdf8544f7cd0fd4c002546f5704b813845beb2a ZendeskSupportSDK: 3a8e508ab1d9dd22dc038df6c694466414e037ba -PODFILE CHECKSUM: e8394e38023e50481a5683373d9efd4e827ba1db +PODFILE CHECKSUM: d72ea2099db46ccdbe3937b108f4dee7149f0575 COCOAPODS: 1.11.2 From 424545a8e104e3d18e9e97ba8c1b220450112bf0 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Mon, 29 Aug 2022 11:14:11 +0700 Subject: [PATCH 003/256] Add WordPressOrgAPI and JetpackConnectionRemote --- .../Networking.xcodeproj/project.pbxproj | 8 ++ .../Remote/JetpackConnectionRemote.swift | 42 ++++++++++ .../Networking/Remote/WordPressOrgAPI.swift | 76 +++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 Networking/Networking/Remote/JetpackConnectionRemote.swift create mode 100644 Networking/Networking/Remote/WordPressOrgAPI.swift diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index 2517ecd6714..337670d64c4 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -624,6 +624,8 @@ DE2095BD27956D7900171F1C /* CouponReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2095BC27956D7900171F1C /* CouponReport.swift */; }; DE2095BF279583A100171F1C /* CouponReportListMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2095BE279583A100171F1C /* CouponReportListMapper.swift */; }; DE2095C127966EC800171F1C /* coupon-reports.json in Resources */ = {isa = PBXBuildFile; fileRef = DE2095C027966EC800171F1C /* coupon-reports.json */; }; + DE34050428BC62C900CF0D97 /* WordPressOrgAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34050328BC62C900CF0D97 /* WordPressOrgAPI.swift */; }; + DE34050628BC62EB00CF0D97 /* JetpackConnectionRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34050528BC62EB00CF0D97 /* JetpackConnectionRemote.swift */; }; DE5CA111288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json in Resources */ = {isa = PBXBuildFile; fileRef = DE5CA110288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json */; }; DE6F308727966FEF004E1C9A /* CouponReportListMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6F308627966FEF004E1C9A /* CouponReportListMapperTests.swift */; }; DE74F29A27E08F5A0002FE59 /* SiteSettingMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE74F29927E08F5A0002FE59 /* SiteSettingMapper.swift */; }; @@ -1305,6 +1307,8 @@ DE2095BC27956D7900171F1C /* CouponReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponReport.swift; sourceTree = ""; }; DE2095BE279583A100171F1C /* CouponReportListMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponReportListMapper.swift; sourceTree = ""; }; DE2095C027966EC800171F1C /* coupon-reports.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "coupon-reports.json"; sourceTree = ""; }; + DE34050328BC62C900CF0D97 /* WordPressOrgAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordPressOrgAPI.swift; sourceTree = ""; }; + DE34050528BC62EB00CF0D97 /* JetpackConnectionRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackConnectionRemote.swift; sourceTree = ""; }; DE5CA110288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-malformed-variations-and-image-alt.json"; sourceTree = ""; }; DE6F308627966FEF004E1C9A /* CouponReportListMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponReportListMapperTests.swift; sourceTree = ""; }; DE74F29927E08F5A0002FE59 /* SiteSettingMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSettingMapper.swift; sourceTree = ""; }; @@ -1677,6 +1681,7 @@ B557DA0520975507005962F4 /* Remote */ = { isa = PBXGroup; children = ( + DE34050328BC62C900CF0D97 /* WordPressOrgAPI.swift */, B557DA0020975500005962F4 /* Remote.swift */, B505F6D020BEE39600BB1B69 /* AccountRemote.swift */, 2685C0FD263B5D8900D9EE97 /* AddOnGroupRemote.swift */, @@ -1716,6 +1721,7 @@ FE28F6E5268429B6004465C7 /* UserRemote.swift */, 077F39D526A58E4500ABEADC /* SystemStatusRemote.swift */, AEF94584272974F2001DCCFB /* TelemetryRemote.swift */, + DE34050528BC62EB00CF0D97 /* JetpackConnectionRemote.swift */, ); path = Remote; sourceTree = ""; @@ -2919,6 +2925,7 @@ CE43A8F9229F463000A4FF29 /* ProductDownload.swift in Sources */, 4513382027A8227F00AE5E78 /* InboxNotesRemote.swift in Sources */, B53EF5322180F21C003E146F /* Dictionary+Woo.swift in Sources */, + DE34050628BC62EB00CF0D97 /* JetpackConnectionRemote.swift in Sources */, 24F98C522502E79800F49B68 /* FeatureFlagsRemote.swift in Sources */, 74A1D26D21189DFF00931DFA /* SiteVisitStatsMapper.swift in Sources */, 45152809257A7C6E0076B03C /* ProductAttributesRemote.swift in Sources */, @@ -2981,6 +2988,7 @@ 020D07BE23D8570800FD9580 /* MediaListMapper.swift in Sources */, 0359EA1327AAC6D00048DE2D /* WCPayCardPaymentDetails.swift in Sources */, CCB2CA9E262091CB00285CA0 /* SuccessDataResultMapper.swift in Sources */, + DE34050428BC62C900CF0D97 /* WordPressOrgAPI.swift in Sources */, 74C8F06820EEB7BD00B6EDC9 /* OrderNotesMapper.swift in Sources */, 24F98C582502EA8800F49B68 /* FeatureFlagMapper.swift in Sources */, 451A9832260B9D2D0059D135 /* ShippingLabelPackagesMapper.swift in Sources */, diff --git a/Networking/Networking/Remote/JetpackConnectionRemote.swift b/Networking/Networking/Remote/JetpackConnectionRemote.swift new file mode 100644 index 00000000000..5bc97c53fb8 --- /dev/null +++ b/Networking/Networking/Remote/JetpackConnectionRemote.swift @@ -0,0 +1,42 @@ +import Foundation +import WordPressKit + +/// Handle API requests to the Jetpack REST API. +/// +public struct JetpackConnectionRemote { + private let api: WordPressOrgAPI + + init(api: WordPressOrgAPI) { + self.api = api + } + + /// Convenience init using site URL and authenticator + /// + public init?(siteURL: String, authenticator: Authenticator) { + guard let baseURL = try? (siteURL + Path.basePath).asURL() else { + return nil + } + self.init(api: WordPressOrgAPI(apiBase: baseURL, authenticator: authenticator)) + } + + /// Fetches the URL for setting up Jetpack connection. + /// + public func fetchJetpackConnectionURL() async throws -> URL? { + let data = try await api.request(method: .get, path: Path.jetpackConnectionURL, parameters: nil) + if let data = data, let escapedString = String(data: data, encoding: .utf8) { + // The API returns an escaped string with double quotes, so we need to clean it up. + let urlString = escapedString + .replacingOccurrences(of: "\"", with: "") + .replacingOccurrences(of: "\\", with: "") + return try urlString.asURL() + } + return nil + } +} + +private extension JetpackConnectionRemote { + enum Path { + static let basePath = "/wp-json/" + static let jetpackConnectionURL = "/jetpack/v4/connection/url" + } +} diff --git a/Networking/Networking/Remote/WordPressOrgAPI.swift b/Networking/Networking/Remote/WordPressOrgAPI.swift new file mode 100644 index 00000000000..a9ec964a20e --- /dev/null +++ b/Networking/Networking/Remote/WordPressOrgAPI.swift @@ -0,0 +1,76 @@ +import Alamofire +import Foundation +import WordPressKit + +/// Error constants for the WordPress.org REST API + +/// - RequestSerializationFailed: The serialization of the request failed +/// +enum WordPressOrgAPIError: Int, Error { + case requestSerializationFailed +} + +/// Class to handle WP.org REST API requests. +/// +final class WordPressOrgAPI { + private let apiBase: URL + private let authenticator: Authenticator? + private let userAgent: String? + + init(apiBase: URL, authenticator: Authenticator? = nil, userAgent: String? = nil) { + self.apiBase = apiBase + self.authenticator = authenticator + self.userAgent = userAgent + } + + func request(method: HTTPMethod, + path: String, + parameters: [String: AnyObject]?) async throws -> Data? { + return try await withCheckedThrowingContinuation { [weak self] continuation in + guard let self = self else { return } + let relativePath = path.removingPrefix("/") + guard let url = URL(string: relativePath, relativeTo: apiBase) else { + return continuation.resume(throwing: WordPressOrgAPIError.requestSerializationFailed) + } + + self.sessionManager.request(url, method: method, parameters: parameters, encoding: URLEncoding.default) + .validate() + .responseData(completionHandler: { (response) in + switch response.result { + case .success(let responseObject): + continuation.resume(returning: responseObject) + case .failure(let error): + DDLogWarn("⚠️ Error requesting \(url): \(error.localizedDescription)") + continuation.resume(throwing: error) + } + + }) + } + } + + /// Cancels all ongoing and makes the session so the object will not fullfil any more request + /// + func invalidateAndCancelTasks() { + sessionManager.session.invalidateAndCancel() + } + + private lazy var sessionManager: Alamofire.SessionManager = { + let sessionConfiguration = URLSessionConfiguration.default + let sessionManager = self.makeSessionManager(configuration: sessionConfiguration) + return sessionManager + }() + + private func makeSessionManager(configuration sessionConfiguration: URLSessionConfiguration) -> Alamofire.SessionManager { + var additionalHeaders: [String: AnyObject] = [:] + if let userAgent = self.userAgent { + additionalHeaders["User-Agent"] = userAgent as AnyObject? + } + + sessionConfiguration.httpAdditionalHeaders = additionalHeaders + + let sessionManager = Alamofire.SessionManager(configuration: sessionConfiguration) + sessionManager.adapter = authenticator + sessionManager.retrier = authenticator + return sessionManager + } +} From d621256186877bae07f5b8f3c70bbfd59637a161 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Mon, 29 Aug 2022 11:14:30 +0700 Subject: [PATCH 004/256] Add new store and action for jetpack connection --- Yosemite/Yosemite.xcodeproj/project.pbxproj | 12 +++++ .../Actions/JetpackConnectionAction.swift | 8 ++++ .../Yosemite/Base/DeauthenticatedStore.swift | 47 +++++++++++++++++++ .../Stores/JetpackConnectionStore.swift | 47 +++++++++++++++++++ 4 files changed, 114 insertions(+) create mode 100644 Yosemite/Yosemite/Actions/JetpackConnectionAction.swift create mode 100644 Yosemite/Yosemite/Base/DeauthenticatedStore.swift create mode 100644 Yosemite/Yosemite/Stores/JetpackConnectionStore.swift diff --git a/Yosemite/Yosemite.xcodeproj/project.pbxproj b/Yosemite/Yosemite.xcodeproj/project.pbxproj index 32db841c1aa..7e600702a54 100644 --- a/Yosemite/Yosemite.xcodeproj/project.pbxproj +++ b/Yosemite/Yosemite.xcodeproj/project.pbxproj @@ -364,6 +364,9 @@ D8C11A5622DFB0BE00D4A88D /* OrderStatsV4Totals+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C11A5522DFB0BE00D4A88D /* OrderStatsV4Totals+ReadOnlyConvertible.swift */; }; D8C11A5822DFB2FF00D4A88D /* OrderStatsV4Interval+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C11A5722DFB2FF00D4A88D /* OrderStatsV4Interval+ReadOnlyConvertible.swift */; }; D8C11A5A22DFC21600D4A88D /* StatsStoreV4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C11A5922DFC21600D4A88D /* StatsStoreV4Tests.swift */; }; + DE3404FC28BC5E7800CF0D97 /* JetpackConnectionAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3404FB28BC5E7800CF0D97 /* JetpackConnectionAction.swift */; }; + DE3404FE28BC5F4200CF0D97 /* JetpackConnectionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3404FD28BC5F4200CF0D97 /* JetpackConnectionStore.swift */; }; + DE34050828BC706B00CF0D97 /* DeauthenticatedStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34050728BC706B00CF0D97 /* DeauthenticatedStore.swift */; }; DE3C5B1D286AEDA10049E6AA /* MockOrderCardPresentPaymentEligibilityActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3C5B1C286AEDA10049E6AA /* MockOrderCardPresentPaymentEligibilityActionHandler.swift */; }; DE3C5B21286BF2270049E6AA /* MockSystemStatusActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3C5B20286BF2270049E6AA /* MockSystemStatusActionHandler.swift */; }; DE3C5B23286C03F90049E6AA /* MockCardPresentPaymentActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3C5B22286C03F80049E6AA /* MockCardPresentPaymentActionHandler.swift */; }; @@ -769,6 +772,9 @@ D8C11A5522DFB0BE00D4A88D /* OrderStatsV4Totals+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrderStatsV4Totals+ReadOnlyConvertible.swift"; sourceTree = ""; }; D8C11A5722DFB2FF00D4A88D /* OrderStatsV4Interval+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrderStatsV4Interval+ReadOnlyConvertible.swift"; sourceTree = ""; }; D8C11A5922DFC21600D4A88D /* StatsStoreV4Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsStoreV4Tests.swift; sourceTree = ""; }; + DE3404FB28BC5E7800CF0D97 /* JetpackConnectionAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackConnectionAction.swift; sourceTree = ""; }; + DE3404FD28BC5F4200CF0D97 /* JetpackConnectionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackConnectionStore.swift; sourceTree = ""; }; + DE34050728BC706B00CF0D97 /* DeauthenticatedStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeauthenticatedStore.swift; sourceTree = ""; }; DE3C5B1C286AEDA10049E6AA /* MockOrderCardPresentPaymentEligibilityActionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOrderCardPresentPaymentEligibilityActionHandler.swift; sourceTree = ""; }; DE3C5B20286BF2270049E6AA /* MockSystemStatusActionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSystemStatusActionHandler.swift; sourceTree = ""; }; DE3C5B22286C03F80049E6AA /* MockCardPresentPaymentActionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCardPresentPaymentActionHandler.swift; sourceTree = ""; }; @@ -1358,6 +1364,7 @@ 077F39DF26A5A6F500ABEADC /* SystemStatusStore.swift */, FE28F6EF26844231004465C7 /* UserStore.swift */, B9AECD3B2850F3C600E78584 /* OrderCardPresentPaymentEligibilityStore.swift */, + DE3404FD28BC5F4200CF0D97 /* JetpackConnectionStore.swift */, ); path = Stores; sourceTree = ""; @@ -1502,6 +1509,7 @@ B5C9DE112087FF0E006B910A /* Store.swift */, 24163B9D257F41A600F94EC3 /* StoresManager.swift */, 24163BA7257F41C500F94EC3 /* SessionManagerProtocol.swift */, + DE34050728BC706B00CF0D97 /* DeauthenticatedStore.swift */, ); path = Base; sourceTree = ""; @@ -1594,6 +1602,7 @@ 077F39DD26A5A1CB00ABEADC /* SystemStatusAction.swift */, FE28F6ED268440B1004465C7 /* UserAction.swift */, B9AECD3D2850F41100E78584 /* OrderCardPresentPaymentEligibilityAction.swift */, + DE3404FB28BC5E7800CF0D97 /* JetpackConnectionAction.swift */, ); path = Actions; sourceTree = ""; @@ -1881,6 +1890,7 @@ CE179D57235F4E7500C24EB3 /* RefundStore.swift in Sources */, 0218B4EE242E08B20083A847 /* MediaType.swift in Sources */, 247CE7C42582DE5300F9D9D1 /* ProductStockStatus+Mocks.swift in Sources */, + DE3404FC28BC5E7800CF0D97 /* JetpackConnectionAction.swift in Sources */, CE3B7AD72225ECA90050FE4B /* OrderStatusStore.swift in Sources */, B5631ECD2114DF8C008D3535 /* EntityListener.swift in Sources */, D80F758A223F72AA002F4A3B /* ShipmentTrackingProviderGroup+ReadOnlyConvertible.swift in Sources */, @@ -1959,6 +1969,7 @@ 26788979270E057900BD249E /* OrderFactory.swift in Sources */, B5B5C797208E49B600642956 /* Action+Internal.swift in Sources */, 74A7688C20D45EBA00F9D437 /* OrderStore.swift in Sources */, + DE3404FE28BC5F4200CF0D97 /* JetpackConnectionStore.swift in Sources */, 2618707C2540B6A4006522A1 /* ShippingLineTax+ReadOnlyConvertible.swift in Sources */, 749375002249605E007D85D1 /* ProductAction.swift in Sources */, D831E2E4230E3524000037D0 /* ProductReviewAction.swift in Sources */, @@ -1993,6 +2004,7 @@ 0248B3652459018100A271A4 /* ResultsController+FilterProducts.swift in Sources */, 02E262C2238CF74D00B79588 /* StorageShippingSettingsService.swift in Sources */, FE28F6EE268440B1004465C7 /* UserAction.swift in Sources */, + DE34050828BC706B00CF0D97 /* DeauthenticatedStore.swift in Sources */, 749375042249691D007D85D1 /* Product+ReadOnlyConvertible.swift in Sources */, D4CBAE6426D4464500BBE6D1 /* AnnouncementsAction.swift in Sources */, CE43A90222A072D800A4FF29 /* ProductDownload+ReadOnlyConvertible.swift in Sources */, diff --git a/Yosemite/Yosemite/Actions/JetpackConnectionAction.swift b/Yosemite/Yosemite/Actions/JetpackConnectionAction.swift new file mode 100644 index 00000000000..269d69c263a --- /dev/null +++ b/Yosemite/Yosemite/Actions/JetpackConnectionAction.swift @@ -0,0 +1,8 @@ +import Foundation +import WordPressKit + +/// Defines actions supported by `JetpackConnectionStore`. +public enum JetpackConnectionAction: Action { + /// Fetches the URL used for setting up Jetpack connection using the given authenticator + case fetchJetpackConnectionURL(siteURL: String, authenticator: Authenticator, completion: (Result) -> Void) +} diff --git a/Yosemite/Yosemite/Base/DeauthenticatedStore.swift b/Yosemite/Yosemite/Base/DeauthenticatedStore.swift new file mode 100644 index 00000000000..a183cc29462 --- /dev/null +++ b/Yosemite/Yosemite/Base/DeauthenticatedStore.swift @@ -0,0 +1,47 @@ +import Foundation +import Networking +import WooFoundation + +// MARK: - DeauthenticatedStore: Holds the data associated to a specific domain of the application in the deauthenticated state. +// Every store is subscribed to the global action dispatcher (although it can be initialized with a custom dispatcher), and should +// respond to relevant Actions by implementing onAction(_:), and change its internal state according to those actions. +// +open class DeauthenticatedStore: ActionsProcessor { + + /// The dispatcher used to subscribe to Actions. + /// + public let dispatcher: Dispatcher + + + /// Initializes a new DeauthenticatedStore. + /// + /// - Parameters: + /// - dispatcher: the Dispatcher to use to receive Actions. + /// + public init(dispatcher: Dispatcher) { + self.dispatcher = dispatcher + + registerSupportedActions(in: dispatcher) + } + + /// Deinitializer + /// + deinit { + dispatcher.unregister(processor: self) + } + + + // MARK: - Dispatcher's Delegate Methods + + /// Subclasses should override this and register for supported Dispatcher Actions. + /// + open func registerSupportedActions(in dispatcher: Dispatcher) { + logErrorAndExit("Override me!") + } + + /// This method is called for every Action. Subclasses should override this and deal with the Actions relevant to them. + /// + open func onAction(_ action: Action) { + logErrorAndExit("Override me!") + } +} diff --git a/Yosemite/Yosemite/Stores/JetpackConnectionStore.swift b/Yosemite/Yosemite/Stores/JetpackConnectionStore.swift new file mode 100644 index 00000000000..9f0c1bbbf05 --- /dev/null +++ b/Yosemite/Yosemite/Stores/JetpackConnectionStore.swift @@ -0,0 +1,47 @@ +import Foundation +import Networking +import WordPressKit + +/// Handles `JetpackConnectionAction` +/// +public final class JetpackConnectionStore: DeauthenticatedStore { + + public override init(dispatcher: Dispatcher) { + super.init(dispatcher: dispatcher) + } + + override public func registerSupportedActions(in dispatcher: Dispatcher) { + dispatcher.register(processor: self, for: JetpackConnectionAction.self) + } + + /// Called whenever a given Action is dispatched. + /// + public override func onAction(_ action: Action) { + guard let action = action as? JetpackConnectionAction else { + assertionFailure("JetpackConnectionStore received an unsupported action") + return + } + switch action { + case let .fetchJetpackConnectionURL(siteURL, authenticator, completion): + fetchJetpackConnectionURL(siteURL: siteURL, with: authenticator, completion: completion) + } + } +} + +private extension JetpackConnectionStore { + func fetchJetpackConnectionURL(siteURL: String, with authenticator: Authenticator, completion: @escaping (Result) -> Void) { + let remote = JetpackConnectionRemote(siteURL: siteURL, authenticator: authenticator) + Task { + do { + let url = try await remote?.fetchJetpackConnectionURL() + await MainActor.run { + completion(.success(url)) + } + } catch let error { + await MainActor.run { + completion(.failure(error)) + } + } + } + } +} From e6e781e3ee29c404ebbdcf4fc80a52729fbfc38d Mon Sep 17 00:00:00 2001 From: Huong Do Date: Mon, 29 Aug 2022 11:15:29 +0700 Subject: [PATCH 005/256] Update DeauthenticatedState with JetpackConnectionStore --- .../Classes/Yosemite/DeauthenticatedState.swift | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/Yosemite/DeauthenticatedState.swift b/WooCommerce/Classes/Yosemite/DeauthenticatedState.swift index 276521e38cc..8ec1fb51d46 100644 --- a/WooCommerce/Classes/Yosemite/DeauthenticatedState.swift +++ b/WooCommerce/Classes/Yosemite/DeauthenticatedState.swift @@ -6,6 +6,17 @@ import Yosemite // MARK: - DeauthenticatedState // class DeauthenticatedState: StoresManagerState { + /// Dispatcher: Glues all of the Stores! + /// + private let dispatcher = Dispatcher() + + /// Retains all of the active Services + /// + private let services: [ActionsProcessor] + + init() { + services = [JetpackConnectionStore(dispatcher: dispatcher)] + } /// NO-OP: Executed when current state is activated. /// @@ -15,7 +26,9 @@ class DeauthenticatedState: StoresManagerState { /// func willLeave() { } - /// NO-OP: During deauth method, we're not running any actions. + /// During deauth method, we're not handling actions that don't require access token. /// - func onAction(_ action: Action) { } + func onAction(_ action: Action) { + dispatcher.dispatch(action) + } } From 29632238752d3639001b30e58aaa14444b6d8306 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 30 Aug 2022 11:26:44 +0700 Subject: [PATCH 006/256] Move WordPressOrgAPI and JetpackConnectionRemote to new group WordPressOrgRemote --- .../Networking.xcodeproj/project.pbxproj | 24 ++++++++++++------- .../JetpackConnectionRemote.swift | 2 +- .../WordPressOrgAPI.swift | 8 +++---- 3 files changed, 21 insertions(+), 13 deletions(-) rename Networking/Networking/{Remote => WordPressOrgRemote}/JetpackConnectionRemote.swift (97%) rename Networking/Networking/{Remote => WordPressOrgRemote}/WordPressOrgAPI.swift (93%) diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index 337670d64c4..a501e952a4f 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -624,8 +624,8 @@ DE2095BD27956D7900171F1C /* CouponReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2095BC27956D7900171F1C /* CouponReport.swift */; }; DE2095BF279583A100171F1C /* CouponReportListMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2095BE279583A100171F1C /* CouponReportListMapper.swift */; }; DE2095C127966EC800171F1C /* coupon-reports.json in Resources */ = {isa = PBXBuildFile; fileRef = DE2095C027966EC800171F1C /* coupon-reports.json */; }; - DE34050428BC62C900CF0D97 /* WordPressOrgAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34050328BC62C900CF0D97 /* WordPressOrgAPI.swift */; }; - DE34050628BC62EB00CF0D97 /* JetpackConnectionRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34050528BC62EB00CF0D97 /* JetpackConnectionRemote.swift */; }; + DE34050E28BDC8D500CF0D97 /* WordPressOrgAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34050C28BDC8D500CF0D97 /* WordPressOrgAPI.swift */; }; + DE34050F28BDC8D500CF0D97 /* JetpackConnectionRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34050D28BDC8D500CF0D97 /* JetpackConnectionRemote.swift */; }; DE5CA111288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json in Resources */ = {isa = PBXBuildFile; fileRef = DE5CA110288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json */; }; DE6F308727966FEF004E1C9A /* CouponReportListMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6F308627966FEF004E1C9A /* CouponReportListMapperTests.swift */; }; DE74F29A27E08F5A0002FE59 /* SiteSettingMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE74F29927E08F5A0002FE59 /* SiteSettingMapper.swift */; }; @@ -1307,8 +1307,8 @@ DE2095BC27956D7900171F1C /* CouponReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponReport.swift; sourceTree = ""; }; DE2095BE279583A100171F1C /* CouponReportListMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponReportListMapper.swift; sourceTree = ""; }; DE2095C027966EC800171F1C /* coupon-reports.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "coupon-reports.json"; sourceTree = ""; }; - DE34050328BC62C900CF0D97 /* WordPressOrgAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordPressOrgAPI.swift; sourceTree = ""; }; - DE34050528BC62EB00CF0D97 /* JetpackConnectionRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackConnectionRemote.swift; sourceTree = ""; }; + DE34050C28BDC8D500CF0D97 /* WordPressOrgAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordPressOrgAPI.swift; sourceTree = ""; }; + DE34050D28BDC8D500CF0D97 /* JetpackConnectionRemote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JetpackConnectionRemote.swift; sourceTree = ""; }; DE5CA110288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-malformed-variations-and-image-alt.json"; sourceTree = ""; }; DE6F308627966FEF004E1C9A /* CouponReportListMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponReportListMapperTests.swift; sourceTree = ""; }; DE74F29927E08F5A0002FE59 /* SiteSettingMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSettingMapper.swift; sourceTree = ""; }; @@ -1644,6 +1644,7 @@ B557D9E5209753AA005962F4 /* Networking */ = { isa = PBXGroup; children = ( + DE34050B28BDC86900CF0D97 /* WordPressOrgRemote */, B5A0369F214C0F4C00774E2C /* Internal */, B5BB1D0A20A204F400112D92 /* Extensions */, B567AF2720A0FA0A00AB6C62 /* Mapper */, @@ -1681,7 +1682,6 @@ B557DA0520975507005962F4 /* Remote */ = { isa = PBXGroup; children = ( - DE34050328BC62C900CF0D97 /* WordPressOrgAPI.swift */, B557DA0020975500005962F4 /* Remote.swift */, B505F6D020BEE39600BB1B69 /* AccountRemote.swift */, 2685C0FD263B5D8900D9EE97 /* AddOnGroupRemote.swift */, @@ -1721,7 +1721,6 @@ FE28F6E5268429B6004465C7 /* UserRemote.swift */, 077F39D526A58E4500ABEADC /* SystemStatusRemote.swift */, AEF94584272974F2001DCCFB /* TelemetryRemote.swift */, - DE34050528BC62EB00CF0D97 /* JetpackConnectionRemote.swift */, ); path = Remote; sourceTree = ""; @@ -2318,6 +2317,15 @@ path = Product; sourceTree = ""; }; + DE34050B28BDC86900CF0D97 /* WordPressOrgRemote */ = { + isa = PBXGroup; + children = ( + DE34050D28BDC8D500CF0D97 /* JetpackConnectionRemote.swift */, + DE34050C28BDC8D500CF0D97 /* WordPressOrgAPI.swift */, + ); + path = WordPressOrgRemote; + sourceTree = ""; + }; DE97C3902861B8CD0042E973 /* Encoder */ = { isa = PBXGroup; children = ( @@ -2925,7 +2933,7 @@ CE43A8F9229F463000A4FF29 /* ProductDownload.swift in Sources */, 4513382027A8227F00AE5E78 /* InboxNotesRemote.swift in Sources */, B53EF5322180F21C003E146F /* Dictionary+Woo.swift in Sources */, - DE34050628BC62EB00CF0D97 /* JetpackConnectionRemote.swift in Sources */, + DE34050F28BDC8D500CF0D97 /* JetpackConnectionRemote.swift in Sources */, 24F98C522502E79800F49B68 /* FeatureFlagsRemote.swift in Sources */, 74A1D26D21189DFF00931DFA /* SiteVisitStatsMapper.swift in Sources */, 45152809257A7C6E0076B03C /* ProductAttributesRemote.swift in Sources */, @@ -2988,7 +2996,7 @@ 020D07BE23D8570800FD9580 /* MediaListMapper.swift in Sources */, 0359EA1327AAC6D00048DE2D /* WCPayCardPaymentDetails.swift in Sources */, CCB2CA9E262091CB00285CA0 /* SuccessDataResultMapper.swift in Sources */, - DE34050428BC62C900CF0D97 /* WordPressOrgAPI.swift in Sources */, + DE34050E28BDC8D500CF0D97 /* WordPressOrgAPI.swift in Sources */, 74C8F06820EEB7BD00B6EDC9 /* OrderNotesMapper.swift in Sources */, 24F98C582502EA8800F49B68 /* FeatureFlagMapper.swift in Sources */, 451A9832260B9D2D0059D135 /* ShippingLabelPackagesMapper.swift in Sources */, diff --git a/Networking/Networking/Remote/JetpackConnectionRemote.swift b/Networking/Networking/WordPressOrgRemote/JetpackConnectionRemote.swift similarity index 97% rename from Networking/Networking/Remote/JetpackConnectionRemote.swift rename to Networking/Networking/WordPressOrgRemote/JetpackConnectionRemote.swift index 5bc97c53fb8..5f794fdad28 100644 --- a/Networking/Networking/Remote/JetpackConnectionRemote.swift +++ b/Networking/Networking/WordPressOrgRemote/JetpackConnectionRemote.swift @@ -6,7 +6,7 @@ import WordPressKit public struct JetpackConnectionRemote { private let api: WordPressOrgAPI - init(api: WordPressOrgAPI) { + public init(api: WordPressOrgAPI) { self.api = api } diff --git a/Networking/Networking/Remote/WordPressOrgAPI.swift b/Networking/Networking/WordPressOrgRemote/WordPressOrgAPI.swift similarity index 93% rename from Networking/Networking/Remote/WordPressOrgAPI.swift rename to Networking/Networking/WordPressOrgRemote/WordPressOrgAPI.swift index a9ec964a20e..1615527726e 100644 --- a/Networking/Networking/Remote/WordPressOrgAPI.swift +++ b/Networking/Networking/WordPressOrgRemote/WordPressOrgAPI.swift @@ -12,7 +12,7 @@ enum WordPressOrgAPIError: Int, Error { /// Class to handle WP.org REST API requests. /// -final class WordPressOrgAPI { +public final class WordPressOrgAPI { private let apiBase: URL private let authenticator: Authenticator? private let userAgent: String? @@ -23,9 +23,9 @@ final class WordPressOrgAPI { self.userAgent = userAgent } - func request(method: HTTPMethod, - path: String, - parameters: [String: AnyObject]?) async throws -> Data? { + public func request(method: HTTPMethod, + path: String, + parameters: [String: AnyObject]?) async throws -> Data? { return try await withCheckedThrowingContinuation { [weak self] continuation in guard let self = self else { return } let relativePath = path.removingPrefix("/") From 495664cd37aa721b4e7610fcd19eda904bdbf16a Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 30 Aug 2022 14:11:19 +0700 Subject: [PATCH 007/256] Remove designated initializer from Network and update other network accordingly --- Networking/Networking/Network/MockNetwork.swift | 11 +---------- Networking/Networking/Network/Network.swift | 7 ------- Networking/Networking/Network/NullNetwork.swift | 1 - 3 files changed, 1 insertion(+), 18 deletions(-) diff --git a/Networking/Networking/Network/MockNetwork.swift b/Networking/Networking/Network/MockNetwork.swift index 95b15399c7d..42d18b71c8e 100644 --- a/Networking/Networking/Network/MockNetwork.swift +++ b/Networking/Networking/Network/MockNetwork.swift @@ -27,13 +27,6 @@ class MockNetwork: Network { /// var requestsForResponseData = [URLRequestConvertible]() - - /// Public Initializer - /// - required init(credentials: Credentials) { } - - /// Dummy convenience initializer. Remember: Real Network wrappers will always need credentials! - /// /// Note: If the useResponseQueue param is `true`, any responses added via `simulateResponse` will stored in a FIFO queue /// and used once for a matching request (then removed from the queue). Subsequent requests will use the next response in the queue, and so on. /// @@ -42,9 +35,7 @@ class MockNetwork: Network { /// /// - Parameter useResponseQueue: Use the response queue. Default is `false`. /// - convenience init(useResponseQueue: Bool = false) { - let dummy = Credentials(username: "", authToken: "", siteAddress: "") - self.init(credentials: dummy) + init(useResponseQueue: Bool = false) { self.useResponseQueue = useResponseQueue } diff --git a/Networking/Networking/Network/Network.swift b/Networking/Networking/Network/Network.swift index 09864f44cfe..5dcc0d37f33 100644 --- a/Networking/Networking/Network/Network.swift +++ b/Networking/Networking/Network/Network.swift @@ -19,13 +19,6 @@ public protocol MultipartFormData { /// public protocol Network { - /// Designated Initializer. - /// - /// - Parameters: - /// - credentials: WordPress.com Credentials. - /// - init(credentials: Credentials) - /// Executes the specified Network Request. Upon completion, the payload will be sent back to the caller as a Data instance. /// /// - Parameters: diff --git a/Networking/Networking/Network/NullNetwork.swift b/Networking/Networking/Network/NullNetwork.swift index f71ee1f3de7..a1e76a641d3 100644 --- a/Networking/Networking/Network/NullNetwork.swift +++ b/Networking/Networking/Network/NullNetwork.swift @@ -8,7 +8,6 @@ import Alamofire /// public final class NullNetwork: Network { public init() { } - public required init(credentials: Credentials) { } public func responseData(for request: URLRequestConvertible, completion: @escaping (Data?, Error?) -> Void) { } From f11b770ed217bfe550965e577cd86c53df7bad20 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 30 Aug 2022 14:12:53 +0700 Subject: [PATCH 008/256] Add WordPressOrgNetwork & Request and JetpackConnectionRemote and Mapper --- .../Networking.xcodeproj/project.pbxproj | 32 ++-- .../Mapper/JetpackConnectionURLMapper.swift | 19 +++ .../Network/WordPressOrgNetwork.swift | 137 ++++++++++++++++++ .../Remote/JetpackConnectionRemote.swift | 28 ++++ .../Requests/WordPressOrgRequest.swift | 54 +++++++ .../JetpackConnectionRemote.swift | 42 ------ .../WordPressOrgRemote/WordPressOrgAPI.swift | 76 ---------- 7 files changed, 254 insertions(+), 134 deletions(-) create mode 100644 Networking/Networking/Mapper/JetpackConnectionURLMapper.swift create mode 100644 Networking/Networking/Network/WordPressOrgNetwork.swift create mode 100644 Networking/Networking/Remote/JetpackConnectionRemote.swift create mode 100644 Networking/Networking/Requests/WordPressOrgRequest.swift delete mode 100644 Networking/Networking/WordPressOrgRemote/JetpackConnectionRemote.swift delete mode 100644 Networking/Networking/WordPressOrgRemote/WordPressOrgAPI.swift diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index a501e952a4f..87a174eb510 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -624,8 +624,10 @@ DE2095BD27956D7900171F1C /* CouponReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2095BC27956D7900171F1C /* CouponReport.swift */; }; DE2095BF279583A100171F1C /* CouponReportListMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2095BE279583A100171F1C /* CouponReportListMapper.swift */; }; DE2095C127966EC800171F1C /* coupon-reports.json in Resources */ = {isa = PBXBuildFile; fileRef = DE2095C027966EC800171F1C /* coupon-reports.json */; }; - DE34050E28BDC8D500CF0D97 /* WordPressOrgAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34050C28BDC8D500CF0D97 /* WordPressOrgAPI.swift */; }; - DE34050F28BDC8D500CF0D97 /* JetpackConnectionRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34050D28BDC8D500CF0D97 /* JetpackConnectionRemote.swift */; }; + DE34051328BDCA5100CF0D97 /* WordPressOrgRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34051228BDCA5100CF0D97 /* WordPressOrgRequest.swift */; }; + DE34051528BDEB1900CF0D97 /* WordPressOrgNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34051428BDEB1900CF0D97 /* WordPressOrgNetwork.swift */; }; + DE34051728BDEB6D00CF0D97 /* JetpackConnectionRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34051628BDEB6D00CF0D97 /* JetpackConnectionRemote.swift */; }; + DE34051928BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34051828BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift */; }; DE5CA111288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json in Resources */ = {isa = PBXBuildFile; fileRef = DE5CA110288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json */; }; DE6F308727966FEF004E1C9A /* CouponReportListMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6F308627966FEF004E1C9A /* CouponReportListMapperTests.swift */; }; DE74F29A27E08F5A0002FE59 /* SiteSettingMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE74F29927E08F5A0002FE59 /* SiteSettingMapper.swift */; }; @@ -1307,8 +1309,10 @@ DE2095BC27956D7900171F1C /* CouponReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponReport.swift; sourceTree = ""; }; DE2095BE279583A100171F1C /* CouponReportListMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponReportListMapper.swift; sourceTree = ""; }; DE2095C027966EC800171F1C /* coupon-reports.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "coupon-reports.json"; sourceTree = ""; }; - DE34050C28BDC8D500CF0D97 /* WordPressOrgAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordPressOrgAPI.swift; sourceTree = ""; }; - DE34050D28BDC8D500CF0D97 /* JetpackConnectionRemote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JetpackConnectionRemote.swift; sourceTree = ""; }; + DE34051228BDCA5100CF0D97 /* WordPressOrgRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressOrgRequest.swift; sourceTree = ""; }; + DE34051428BDEB1900CF0D97 /* WordPressOrgNetwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordPressOrgNetwork.swift; sourceTree = ""; }; + DE34051628BDEB6D00CF0D97 /* JetpackConnectionRemote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JetpackConnectionRemote.swift; sourceTree = ""; }; + DE34051828BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackConnectionURLMapper.swift; sourceTree = ""; }; DE5CA110288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-malformed-variations-and-image-alt.json"; sourceTree = ""; }; DE6F308627966FEF004E1C9A /* CouponReportListMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponReportListMapperTests.swift; sourceTree = ""; }; DE74F29927E08F5A0002FE59 /* SiteSettingMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSettingMapper.swift; sourceTree = ""; }; @@ -1539,6 +1543,7 @@ B518662020A097B200037A38 /* Network */ = { isa = PBXGroup; children = ( + DE34051428BDEB1900CF0D97 /* WordPressOrgNetwork.swift */, B518662120A097C200037A38 /* Network.swift */, B556FD68211CE2EC00B5DAE7 /* NetworkError.swift */, B518662320A099BF00037A38 /* AlamofireNetwork.swift */, @@ -1644,7 +1649,6 @@ B557D9E5209753AA005962F4 /* Networking */ = { isa = PBXGroup; children = ( - DE34050B28BDC86900CF0D97 /* WordPressOrgRemote */, B5A0369F214C0F4C00774E2C /* Internal */, B5BB1D0A20A204F400112D92 /* Extensions */, B567AF2720A0FA0A00AB6C62 /* Mapper */, @@ -1683,6 +1687,7 @@ isa = PBXGroup; children = ( B557DA0020975500005962F4 /* Remote.swift */, + DE34051628BDEB6D00CF0D97 /* JetpackConnectionRemote.swift */, B505F6D020BEE39600BB1B69 /* AccountRemote.swift */, 2685C0FD263B5D8900D9EE97 /* AddOnGroupRemote.swift */, 740CF89821937A030023ED3A /* CommentRemote.swift */, @@ -1731,6 +1736,7 @@ B567AF2420A0CCA300AB6C62 /* AuthenticatedRequest.swift */, B557DA0E20975E07005962F4 /* DotcomRequest.swift */, B557D9FF209754FF005962F4 /* JetpackRequest.swift */, + DE34051228BDCA5100CF0D97 /* WordPressOrgRequest.swift */, ); path = Requests; sourceTree = ""; @@ -2146,6 +2152,7 @@ 02BE0A7A274B695F001176D2 /* WordPressMediaMapper.swift */, 02C112772742862600F4F0B4 /* WordPressSiteSettingsMapper.swift */, 0359EA1C27AADE000048DE2D /* WCPayChargeMapper.swift */, + DE34051828BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift */, ); path = Mapper; sourceTree = ""; @@ -2317,15 +2324,6 @@ path = Product; sourceTree = ""; }; - DE34050B28BDC86900CF0D97 /* WordPressOrgRemote */ = { - isa = PBXGroup; - children = ( - DE34050D28BDC8D500CF0D97 /* JetpackConnectionRemote.swift */, - DE34050C28BDC8D500CF0D97 /* WordPressOrgAPI.swift */, - ); - path = WordPressOrgRemote; - sourceTree = ""; - }; DE97C3902861B8CD0042E973 /* Encoder */ = { isa = PBXGroup; children = ( @@ -2763,6 +2761,7 @@ 26455E2425F66982008A1D32 /* ProductAttributeTermRemote.swift in Sources */, 7426CA0D21AF27B9004E9FFC /* SiteAPIRemote.swift in Sources */, 451A97D1260A03900059D135 /* ShippingLabelCustomPackage.swift in Sources */, + DE34051328BDCA5100CF0D97 /* WordPressOrgRequest.swift in Sources */, D88D5A45230BC6F9007B6E01 /* ProductReviewsRemote.swift in Sources */, B59325D4217E4206000B0E8E /* NoteBlock.swift in Sources */, DEC51AF92769A212009F3DF4 /* SystemStatus+Settings.swift in Sources */, @@ -2917,6 +2916,7 @@ 31884A3B2603F3C7003FE338 /* SitePluginStatusEnum.swift in Sources */, 3192F21C260D32550067FEF9 /* WCPayAccountMapper.swift in Sources */, CE50345E21B571A7007573C6 /* SitePlanMapper.swift in Sources */, + DE34051528BDEB1900CF0D97 /* WordPressOrgNetwork.swift in Sources */, 3192F224260D34C40067FEF9 /* WCPayAccountStatusEnum.swift in Sources */, D8FBFF2022D52553006E3336 /* OrderStatsV4Totals.swift in Sources */, 02C254B925637BA000A04423 /* OrderShippingLabelListMapper.swift in Sources */, @@ -2933,7 +2933,6 @@ CE43A8F9229F463000A4FF29 /* ProductDownload.swift in Sources */, 4513382027A8227F00AE5E78 /* InboxNotesRemote.swift in Sources */, B53EF5322180F21C003E146F /* Dictionary+Woo.swift in Sources */, - DE34050F28BDC8D500CF0D97 /* JetpackConnectionRemote.swift in Sources */, 24F98C522502E79800F49B68 /* FeatureFlagsRemote.swift in Sources */, 74A1D26D21189DFF00931DFA /* SiteVisitStatsMapper.swift in Sources */, 45152809257A7C6E0076B03C /* ProductAttributesRemote.swift in Sources */, @@ -2984,6 +2983,7 @@ 026CF61A237D607A009563D4 /* ProductVariationAttribute.swift in Sources */, D8FBFF1A22D4DF7A006E3336 /* OrderStatsV4.swift in Sources */, 74A1D26B21189B8100931DFA /* SiteVisitStatsItem.swift in Sources */, + DE34051728BDEB6D00CF0D97 /* JetpackConnectionRemote.swift in Sources */, B505F6EC20BEFDC200BB1B69 /* Loader.swift in Sources */, 74D3BD142114FE6900A6E85E /* MIContainer.swift in Sources */, 314703082670222500EF253A /* PaymentGatewayAccount.swift in Sources */, @@ -2996,7 +2996,6 @@ 020D07BE23D8570800FD9580 /* MediaListMapper.swift in Sources */, 0359EA1327AAC6D00048DE2D /* WCPayCardPaymentDetails.swift in Sources */, CCB2CA9E262091CB00285CA0 /* SuccessDataResultMapper.swift in Sources */, - DE34050E28BDC8D500CF0D97 /* WordPressOrgAPI.swift in Sources */, 74C8F06820EEB7BD00B6EDC9 /* OrderNotesMapper.swift in Sources */, 24F98C582502EA8800F49B68 /* FeatureFlagMapper.swift in Sources */, 451A9832260B9D2D0059D135 /* ShippingLabelPackagesMapper.swift in Sources */, @@ -3032,6 +3031,7 @@ DEC51AE927687AAF009F3DF4 /* SystemPluginMapper.swift in Sources */, B557DA0B20975D7E005962F4 /* WooAPIVersion.swift in Sources */, 45150A9E26836A57006922EA /* CountryListMapper.swift in Sources */, + DE34051928BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift in Sources */, CE6BFEE82236D133005C79FB /* ProductDimensions.swift in Sources */, 077F39D426A58DE700ABEADC /* SystemStatusMapper.swift in Sources */, 45152811257A81730076B03C /* ProductAttributeMapper.swift in Sources */, diff --git a/Networking/Networking/Mapper/JetpackConnectionURLMapper.swift b/Networking/Networking/Mapper/JetpackConnectionURLMapper.swift new file mode 100644 index 00000000000..12d09ada438 --- /dev/null +++ b/Networking/Networking/Mapper/JetpackConnectionURLMapper.swift @@ -0,0 +1,19 @@ +import Foundation + +/// Mapper: Jetpack Connection URL +/// +struct JetpackConnectionURLMapper: Mapper { + + /// (Attempts) to convert the response into a URL. + /// + func map(response: Data) throws -> URL? { + guard let escapedString = String(data: response, encoding: .utf8) else { + return nil + } + // The API returns an escaped string with double quotes, so we need to clean it up. + let urlString = escapedString + .replacingOccurrences(of: "\"", with: "") + .replacingOccurrences(of: "\\", with: "") + return try urlString.asURL() + } +} diff --git a/Networking/Networking/Network/WordPressOrgNetwork.swift b/Networking/Networking/Network/WordPressOrgNetwork.swift new file mode 100644 index 00000000000..e9083b2b97e --- /dev/null +++ b/Networking/Networking/Network/WordPressOrgNetwork.swift @@ -0,0 +1,137 @@ +import Alamofire +import Combine +import Foundation +import WordPressKit + +/// Class to handle WP.org REST API requests. +/// +public final class WordPressOrgNetwork: Network { + + private let authenticator: Authenticator? + private let userAgent: String? + + private lazy var sessionManager: Alamofire.SessionManager = { + let sessionConfiguration = URLSessionConfiguration.default + let sessionManager = makeSessionManager(configuration: sessionConfiguration) + return sessionManager + }() + + private lazy var backgroundSessionManager: Alamofire.SessionManager = { + // A unique ID is included in the background session identifier so that the session does not get invalidated when the initializer is called multiple + // times (e.g. when logging in). + let uniqueID = UUID().uuidString + let sessionConfiguration = URLSessionConfiguration.background(withIdentifier: "com.automattic.woocommerce.backgroundsession.\(uniqueID)") + let sessionManager = makeSessionManager(configuration: sessionConfiguration) + return sessionManager + }() + + public init(authenticator: Authenticator? = nil, userAgent: String? = nil) { + self.authenticator = authenticator + self.userAgent = userAgent + } + + public func responseData(for request: URLRequestConvertible) async throws -> Data? { + return try await withCheckedThrowingContinuation { [weak self] continuation in + guard let self = self else { return } + + self.sessionManager.request(request) + .validate() + .responseData(completionHandler: { (response) in + switch response.result { + case .success(let responseObject): + continuation.resume(returning: responseObject) + case .failure(let error): + DDLogWarn("⚠️ Error requesting \(request.urlRequest?.url?.absoluteString ?? ""): \(error.localizedDescription)") + continuation.resume(throwing: error) + } + + }) + } + } + + /// Executes the specified Network Request. Upon completion, the payload will be sent back to the caller as a Data instance. + /// + /// - Important: + /// - User agent and authenticator from the initializer will be injected. + /// + /// - Parameters: + /// - request: Request that should be performed. + /// - completion: Closure to be executed upon completion. + /// + /// - Note: + /// - The response body will always be returned (when possible), even when there's a networking error. + /// This differs slightly from the standard Alamofire `.validate()` behavior, and it's required so that + /// the upper layers can properly detect "Jetpack Tunnel" Errors. + /// - Yes. We do the above because the Jetpack Tunnel endpoint doesn't properly relay the correct statusCode. + /// + public func responseData(for request: URLRequestConvertible, completion: @escaping (Data?, Error?) -> Void) { + sessionManager.request(request) + .responseData { response in + completion(response.value, response.networkingError) + } + } + + /// Executes the specified Network Request. Upon completion, the payload will be sent back to the caller as a Data instance. + /// + /// - Important: + /// - User agent and authenticator from the initializer will be injected. + /// + /// - Parameters: + /// - request: Request that should be performed. + /// - completion: Closure to be executed upon completion. + /// + public func responseData(for request: URLRequestConvertible, completion: @escaping (Swift.Result) -> Void) { + sessionManager.request(request).responseData { response in + completion(response.result.toSwiftResult()) + } + } + + /// Executes the specified Network Request. Upon completion, the payload or error will be emitted to the publisher. + /// Only one value will be emitted and the request cannot be retried. + /// + /// - Important: + /// - User agent and authenticator from the initializer will be injected. + /// + /// - Parameter request: Request that should be performed. + /// - Returns: A publisher that emits the result of the given request. + public func responseDataPublisher(for request: URLRequestConvertible) -> AnyPublisher, Never> { + return Future() { [weak self] promise in + guard let self = self else { return } + self.sessionManager.request(request).responseData { response in + let result = response.result.toSwiftResult() + promise(Swift.Result.success(result)) + } + }.eraseToAnyPublisher() + } + + public func uploadMultipartFormData(multipartFormData: @escaping (MultipartFormData) -> Void, + to request: URLRequestConvertible, + completion: @escaping (Data?, Error?) -> Void) { + backgroundSessionManager.upload(multipartFormData: multipartFormData, with: request) { (encodingResult) in + switch encodingResult { + case .success(let upload, _, _): + upload.responseData { response in + completion(response.value, response.error) + } + case .failure(let error): + completion(nil, error) + } + } + } +} + +private extension WordPressOrgNetwork { + func makeSessionManager(configuration sessionConfiguration: URLSessionConfiguration) -> Alamofire.SessionManager { + var additionalHeaders: [String: AnyObject] = [:] + if let userAgent = self.userAgent { + additionalHeaders["User-Agent"] = userAgent as AnyObject? + } + + sessionConfiguration.httpAdditionalHeaders = additionalHeaders + + let sessionManager = Alamofire.SessionManager(configuration: sessionConfiguration) + sessionManager.adapter = authenticator + sessionManager.retrier = authenticator + return sessionManager + } +} diff --git a/Networking/Networking/Remote/JetpackConnectionRemote.swift b/Networking/Networking/Remote/JetpackConnectionRemote.swift new file mode 100644 index 00000000000..2e4244f27c6 --- /dev/null +++ b/Networking/Networking/Remote/JetpackConnectionRemote.swift @@ -0,0 +1,28 @@ +import Foundation +import WordPressKit + +/// Handle API requests to the Jetpack REST API. +/// +public final class JetpackConnectionRemote: Remote { + private let siteURL: String + + public init(siteURL: String, network: Network) { + self.siteURL = siteURL + super.init(network: network) + } + + /// Fetches the URL for setting up Jetpack connection. + /// + public func fetchJetpackConnectionURL(completion: @escaping (Result) -> Void) { + let request = WordPressOrgRequest(baseURL: siteURL, method: .get, path: Path.jetpackConnectionURL) + let mapper = JetpackConnectionURLMapper() + + enqueue(request, mapper: mapper, completion: completion) + } +} + +private extension JetpackConnectionRemote { + enum Path { + static let jetpackConnectionURL = "/jetpack/v4/connection/url" + } +} diff --git a/Networking/Networking/Requests/WordPressOrgRequest.swift b/Networking/Networking/Requests/WordPressOrgRequest.swift new file mode 100644 index 00000000000..57fa63893c9 --- /dev/null +++ b/Networking/Networking/Requests/WordPressOrgRequest.swift @@ -0,0 +1,54 @@ +import Foundation +import Alamofire + +/// Represents a WordPress.org REST API Endpoint +/// +struct WordPressOrgRequest: URLRequestConvertible { + + /// Base URL for the endpoint + /// + let baseURL: String + + /// HTTP Request Method + /// + let method: HTTPMethod + + /// Path to endpoint + /// + let path: String + + /// Parameters + /// + let parameters: [String: Any] + + + /// Designated Initializer. + /// + /// - Parameters: + /// - method: HTTP Method we should use. + /// - path: RPC that should be called. + /// - parameters: Collection of Key/Value parameters. + /// + init(baseURL: String, method: HTTPMethod, path: String, parameters: [String: Any]? = nil) { + self.baseURL = baseURL + self.method = method + self.path = path + self.parameters = parameters ?? [:] + } + + + /// Returns a URLRequest instance reprensenting the current Jetpack Request. + /// + func asURLRequest() throws -> URLRequest { + let url = URL(string: baseURL + Settings.basePath + path.removingPrefix("/"))! + let request = try URLRequest(url: url, method: method, headers: nil) + + return try URLEncoding.default.encode(request, with: parameters) + } +} + +private extension WordPressOrgRequest { + enum Settings { + static let basePath = "/wp-json/" + } +} diff --git a/Networking/Networking/WordPressOrgRemote/JetpackConnectionRemote.swift b/Networking/Networking/WordPressOrgRemote/JetpackConnectionRemote.swift deleted file mode 100644 index 5f794fdad28..00000000000 --- a/Networking/Networking/WordPressOrgRemote/JetpackConnectionRemote.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation -import WordPressKit - -/// Handle API requests to the Jetpack REST API. -/// -public struct JetpackConnectionRemote { - private let api: WordPressOrgAPI - - public init(api: WordPressOrgAPI) { - self.api = api - } - - /// Convenience init using site URL and authenticator - /// - public init?(siteURL: String, authenticator: Authenticator) { - guard let baseURL = try? (siteURL + Path.basePath).asURL() else { - return nil - } - self.init(api: WordPressOrgAPI(apiBase: baseURL, authenticator: authenticator)) - } - - /// Fetches the URL for setting up Jetpack connection. - /// - public func fetchJetpackConnectionURL() async throws -> URL? { - let data = try await api.request(method: .get, path: Path.jetpackConnectionURL, parameters: nil) - if let data = data, let escapedString = String(data: data, encoding: .utf8) { - // The API returns an escaped string with double quotes, so we need to clean it up. - let urlString = escapedString - .replacingOccurrences(of: "\"", with: "") - .replacingOccurrences(of: "\\", with: "") - return try urlString.asURL() - } - return nil - } -} - -private extension JetpackConnectionRemote { - enum Path { - static let basePath = "/wp-json/" - static let jetpackConnectionURL = "/jetpack/v4/connection/url" - } -} diff --git a/Networking/Networking/WordPressOrgRemote/WordPressOrgAPI.swift b/Networking/Networking/WordPressOrgRemote/WordPressOrgAPI.swift deleted file mode 100644 index 1615527726e..00000000000 --- a/Networking/Networking/WordPressOrgRemote/WordPressOrgAPI.swift +++ /dev/null @@ -1,76 +0,0 @@ -import Alamofire -import Foundation -import WordPressKit - -/// Error constants for the WordPress.org REST API - -/// - RequestSerializationFailed: The serialization of the request failed -/// -enum WordPressOrgAPIError: Int, Error { - case requestSerializationFailed -} - -/// Class to handle WP.org REST API requests. -/// -public final class WordPressOrgAPI { - private let apiBase: URL - private let authenticator: Authenticator? - private let userAgent: String? - - init(apiBase: URL, authenticator: Authenticator? = nil, userAgent: String? = nil) { - self.apiBase = apiBase - self.authenticator = authenticator - self.userAgent = userAgent - } - - public func request(method: HTTPMethod, - path: String, - parameters: [String: AnyObject]?) async throws -> Data? { - return try await withCheckedThrowingContinuation { [weak self] continuation in - guard let self = self else { return } - let relativePath = path.removingPrefix("/") - guard let url = URL(string: relativePath, relativeTo: apiBase) else { - return continuation.resume(throwing: WordPressOrgAPIError.requestSerializationFailed) - } - - self.sessionManager.request(url, method: method, parameters: parameters, encoding: URLEncoding.default) - .validate() - .responseData(completionHandler: { (response) in - switch response.result { - case .success(let responseObject): - continuation.resume(returning: responseObject) - case .failure(let error): - DDLogWarn("⚠️ Error requesting \(url): \(error.localizedDescription)") - continuation.resume(throwing: error) - } - - }) - } - } - - /// Cancels all ongoing and makes the session so the object will not fullfil any more request - /// - func invalidateAndCancelTasks() { - sessionManager.session.invalidateAndCancel() - } - - private lazy var sessionManager: Alamofire.SessionManager = { - let sessionConfiguration = URLSessionConfiguration.default - let sessionManager = self.makeSessionManager(configuration: sessionConfiguration) - return sessionManager - }() - - private func makeSessionManager(configuration sessionConfiguration: URLSessionConfiguration) -> Alamofire.SessionManager { - var additionalHeaders: [String: AnyObject] = [:] - if let userAgent = self.userAgent { - additionalHeaders["User-Agent"] = userAgent as AnyObject? - } - - sessionConfiguration.httpAdditionalHeaders = additionalHeaders - - let sessionManager = Alamofire.SessionManager(configuration: sessionConfiguration) - sessionManager.adapter = authenticator - sessionManager.retrier = authenticator - return sessionManager - } -} From 91ecfcf7431e9c66843a2ccbe64e13eaf07649d5 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 30 Aug 2022 14:13:20 +0700 Subject: [PATCH 009/256] Make extension for Alamofire DataResponse and Result internal --- Networking/Networking/Network/AlamofireNetwork.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Networking/Networking/Network/AlamofireNetwork.swift b/Networking/Networking/Network/AlamofireNetwork.swift index ce738f976ad..991cbd0c5bd 100644 --- a/Networking/Networking/Network/AlamofireNetwork.swift +++ b/Networking/Networking/Network/AlamofireNetwork.swift @@ -109,7 +109,7 @@ public class AlamofireNetwork: Network { /// MARK: - Alamofire.DataResponse: Private Methods /// -private extension Alamofire.DataResponse { +extension Alamofire.DataResponse { /// Returns the Networking Layer Error (if any): /// @@ -137,7 +137,7 @@ private extension Alamofire.DataResponse { // MARK: - Swift.Result Conversion -private extension Alamofire.Result { +extension Alamofire.Result { /// Convert this `Alamofire.Result` to a `Swift.Result`. /// func toSwiftResult() -> Swift.Result { From c2aad3c57579254768175951be212ef9ad0a2e0d Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 30 Aug 2022 14:13:41 +0700 Subject: [PATCH 010/256] Update JetpackConnectionStore with the new updates of network and remote --- .../Yosemite/Stores/JetpackConnectionStore.swift | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/Yosemite/Yosemite/Stores/JetpackConnectionStore.swift b/Yosemite/Yosemite/Stores/JetpackConnectionStore.swift index 9f0c1bbbf05..47afa726643 100644 --- a/Yosemite/Yosemite/Stores/JetpackConnectionStore.swift +++ b/Yosemite/Yosemite/Stores/JetpackConnectionStore.swift @@ -30,18 +30,8 @@ public final class JetpackConnectionStore: DeauthenticatedStore { private extension JetpackConnectionStore { func fetchJetpackConnectionURL(siteURL: String, with authenticator: Authenticator, completion: @escaping (Result) -> Void) { - let remote = JetpackConnectionRemote(siteURL: siteURL, authenticator: authenticator) - Task { - do { - let url = try await remote?.fetchJetpackConnectionURL() - await MainActor.run { - completion(.success(url)) - } - } catch let error { - await MainActor.run { - completion(.failure(error)) - } - } - } + let network = WordPressOrgNetwork(authenticator: authenticator, userAgent: UserAgent.defaultUserAgent) + let remote = JetpackConnectionRemote(siteURL: siteURL, network: network) + remote.fetchJetpackConnectionURL(completion: completion) } } From e92c61a3dba77c73d4e1f8af05069a4ad834cf45 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 30 Aug 2022 14:19:56 +0700 Subject: [PATCH 011/256] Add test json response for jetpack connection url request --- Networking/Networking.xcodeproj/project.pbxproj | 4 ++++ Networking/Networking/Network/WordPressOrgNetwork.swift | 2 +- .../NetworkingTests/Responses/jetpack-connection-url.json | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 Networking/NetworkingTests/Responses/jetpack-connection-url.json diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index 87a174eb510..9b1255b94f1 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -628,6 +628,7 @@ DE34051528BDEB1900CF0D97 /* WordPressOrgNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34051428BDEB1900CF0D97 /* WordPressOrgNetwork.swift */; }; DE34051728BDEB6D00CF0D97 /* JetpackConnectionRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34051628BDEB6D00CF0D97 /* JetpackConnectionRemote.swift */; }; DE34051928BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34051828BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift */; }; + DE34051B28BDF12C00CF0D97 /* jetpack-connection-url.json in Resources */ = {isa = PBXBuildFile; fileRef = DE34051A28BDF12C00CF0D97 /* jetpack-connection-url.json */; }; DE5CA111288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json in Resources */ = {isa = PBXBuildFile; fileRef = DE5CA110288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json */; }; DE6F308727966FEF004E1C9A /* CouponReportListMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6F308627966FEF004E1C9A /* CouponReportListMapperTests.swift */; }; DE74F29A27E08F5A0002FE59 /* SiteSettingMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE74F29927E08F5A0002FE59 /* SiteSettingMapper.swift */; }; @@ -1313,6 +1314,7 @@ DE34051428BDEB1900CF0D97 /* WordPressOrgNetwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordPressOrgNetwork.swift; sourceTree = ""; }; DE34051628BDEB6D00CF0D97 /* JetpackConnectionRemote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JetpackConnectionRemote.swift; sourceTree = ""; }; DE34051828BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackConnectionURLMapper.swift; sourceTree = ""; }; + DE34051A28BDF12C00CF0D97 /* jetpack-connection-url.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "jetpack-connection-url.json"; sourceTree = ""; }; DE5CA110288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-malformed-variations-and-image-alt.json"; sourceTree = ""; }; DE6F308627966FEF004E1C9A /* CouponReportListMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponReportListMapperTests.swift; sourceTree = ""; }; DE74F29927E08F5A0002FE59 /* SiteSettingMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSettingMapper.swift; sourceTree = ""; }; @@ -1838,6 +1840,7 @@ B559EBA820A0B5B100836CD4 /* Responses */ = { isa = PBXGroup; children = ( + DE34051A28BDF12C00CF0D97 /* jetpack-connection-url.json */, EE8A86F0286C5226003E8AA4 /* media-update-product-id-in-wordpress-site.json */, D865CE6D278CC19A002C8520 /* stripe-location-error.json */, D865CE6C278CC19A002C8520 /* stripe-location.json */, @@ -2478,6 +2481,7 @@ 3158A4A32729F42500C3CFA8 /* wcpay-account-dev-test.json in Resources */, 31104E142630DDA700587C1E /* wcpay-account-wrong-json.json in Resources */, 3158FE7C26129E2100E566B9 /* wcpay-account-restricted-pending.json in Resources */, + DE34051B28BDF12C00CF0D97 /* jetpack-connection-url.json in Resources */, D823D91422377EE600C90817 /* shipment_tracking_providers.json in Resources */, 4599FC5C24A6276F0056157A /* product-tags-all.json in Resources */, 03DCB77E262738E300C8953D /* coupon.json in Resources */, diff --git a/Networking/Networking/Network/WordPressOrgNetwork.swift b/Networking/Networking/Network/WordPressOrgNetwork.swift index e9083b2b97e..020ce7a6a14 100644 --- a/Networking/Networking/Network/WordPressOrgNetwork.swift +++ b/Networking/Networking/Network/WordPressOrgNetwork.swift @@ -6,7 +6,7 @@ import WordPressKit /// Class to handle WP.org REST API requests. /// public final class WordPressOrgNetwork: Network { - + private let authenticator: Authenticator? private let userAgent: String? diff --git a/Networking/NetworkingTests/Responses/jetpack-connection-url.json b/Networking/NetworkingTests/Responses/jetpack-connection-url.json new file mode 100644 index 00000000000..46558f03931 --- /dev/null +++ b/Networking/NetworkingTests/Responses/jetpack-connection-url.json @@ -0,0 +1 @@ +"https://jetpack.wordpress.com/jetpack.authorize/1/?response_type=code&client_id=209946457&redirect_uri=https%3A%2F%2Ftest.jurassic.ninja%2Fwp-admin%2Fadmin.php%3Fhandler%3Djetpack-connection-webhooks%26action%3Dauthorize%26_wpnonce%3D905fcec%26redirect%3Dhttps%253A%252F%252Ftest.jurassic.ninja%252Fwp-admin%252Fadmin.php%253Fpage%253Djetpack&state=2&scope=editor%3A90c774bc742a8e35288b4aeaf259d0d0&user_email=test%40gmail.com&user_login=test&is_active=1&jp_version=11.2&auth_type=calypso&secret=hTtWoXu0eMS5Edl7aM6k99QuzkLWjCa0&blogname=Test&site_url=https%3A%2F%2Ftest.jurassic.ninja&home_url=https%3A%2F%2Ftest.jurassic.ninja&site_icon&site_lang=en_US&site_created=2022-08-24+07%3A20%3A24&allow_site_connection&locale=en&_ui=jetpack%3AFQfiXtAi%2Bc4QuCgXpeZju4ND&_ut=anon&_as=wp-cli" From 598a10c3150a0f45b54248a4e489b91003f4f5a1 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 30 Aug 2022 14:40:45 +0700 Subject: [PATCH 012/256] Add tests for JetpackConnectionURLMapper --- .../Networking.xcodeproj/project.pbxproj | 4 +++ .../Mapper/JetpackConnectionURLMapper.swift | 1 + .../JetpackConnectionURLMapperTests.swift | 26 +++++++++++++++++++ .../Responses/jetpack-connection-url.json | 2 +- 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 Networking/NetworkingTests/Mapper/JetpackConnectionURLMapperTests.swift diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index 9b1255b94f1..6a233890dd6 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -629,6 +629,7 @@ DE34051728BDEB6D00CF0D97 /* JetpackConnectionRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34051628BDEB6D00CF0D97 /* JetpackConnectionRemote.swift */; }; DE34051928BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34051828BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift */; }; DE34051B28BDF12C00CF0D97 /* jetpack-connection-url.json in Resources */ = {isa = PBXBuildFile; fileRef = DE34051A28BDF12C00CF0D97 /* jetpack-connection-url.json */; }; + DE34051D28BDF1C900CF0D97 /* JetpackConnectionURLMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34051C28BDF1C900CF0D97 /* JetpackConnectionURLMapperTests.swift */; }; DE5CA111288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json in Resources */ = {isa = PBXBuildFile; fileRef = DE5CA110288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json */; }; DE6F308727966FEF004E1C9A /* CouponReportListMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6F308627966FEF004E1C9A /* CouponReportListMapperTests.swift */; }; DE74F29A27E08F5A0002FE59 /* SiteSettingMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE74F29927E08F5A0002FE59 /* SiteSettingMapper.swift */; }; @@ -1315,6 +1316,7 @@ DE34051628BDEB6D00CF0D97 /* JetpackConnectionRemote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JetpackConnectionRemote.swift; sourceTree = ""; }; DE34051828BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackConnectionURLMapper.swift; sourceTree = ""; }; DE34051A28BDF12C00CF0D97 /* jetpack-connection-url.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "jetpack-connection-url.json"; sourceTree = ""; }; + DE34051C28BDF1C900CF0D97 /* JetpackConnectionURLMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackConnectionURLMapperTests.swift; sourceTree = ""; }; DE5CA110288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-malformed-variations-and-image-alt.json"; sourceTree = ""; }; DE6F308627966FEF004E1C9A /* CouponReportListMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponReportListMapperTests.swift; sourceTree = ""; }; DE74F29927E08F5A0002FE59 /* SiteSettingMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSettingMapper.swift; sourceTree = ""; }; @@ -2275,6 +2277,7 @@ CC0786C8267BB32800BA9AC1 /* ShippingLabelStatusMapperTests.swift */, 02C254D22563992900A04423 /* OrderShippingLabelListMapperTests.swift */, FE28F6E926842E49004465C7 /* UserMapperTests.swift */, + DE34051C28BDF1C900CF0D97 /* JetpackConnectionURLMapperTests.swift */, 0359EA1E27AAE4680048DE2D /* WCPayChargeMapperTests.swift */, ); path = Mapper; @@ -3071,6 +3074,7 @@ buildActionMask = 2147483647; files = ( 45551F142523E7FF007EF104 /* UserAgentTests.swift in Sources */, + DE34051D28BDF1C900CF0D97 /* JetpackConnectionURLMapperTests.swift in Sources */, 451A9836260B9DF90059D135 /* ShippingLabelPackagesMapperTests.swift in Sources */, 02BDB83723EA9C4D00BCC63E /* String+HTMLTests.swift in Sources */, 74CF5E8421402C04000CED0A /* TopEarnerStatsRemoteTests.swift in Sources */, diff --git a/Networking/Networking/Mapper/JetpackConnectionURLMapper.swift b/Networking/Networking/Mapper/JetpackConnectionURLMapper.swift index 12d09ada438..737c59adab0 100644 --- a/Networking/Networking/Mapper/JetpackConnectionURLMapper.swift +++ b/Networking/Networking/Mapper/JetpackConnectionURLMapper.swift @@ -12,6 +12,7 @@ struct JetpackConnectionURLMapper: Mapper { } // The API returns an escaped string with double quotes, so we need to clean it up. let urlString = escapedString + .trimmingCharacters(in: .whitespacesAndNewlines) .replacingOccurrences(of: "\"", with: "") .replacingOccurrences(of: "\\", with: "") return try urlString.asURL() diff --git a/Networking/NetworkingTests/Mapper/JetpackConnectionURLMapperTests.swift b/Networking/NetworkingTests/Mapper/JetpackConnectionURLMapperTests.swift new file mode 100644 index 00000000000..ef5a6c723a5 --- /dev/null +++ b/Networking/NetworkingTests/Mapper/JetpackConnectionURLMapperTests.swift @@ -0,0 +1,26 @@ +import XCTest +@testable import Networking + +/// UserMapper Unit Tests +/// +final class JetpackConnectionURLMapperTests: XCTestCase { + + func test_url_is_properly_parsed() { + guard let url = mapURLFromMockResponse() else { + XCTFail() + return + } + let expectedURL = "https://jetpack.wordpress.com/jetpack.authorize/1/?response_type=code&client_id=2099457" + assertEqual(url.absoluteString, expectedURL) + } +} + +private extension JetpackConnectionURLMapperTests { + func mapURLFromMockResponse() -> URL? { + guard let response = Loader.contentsOf("jetpack-connection-url") else { + return nil + } + + return try? JetpackConnectionURLMapper().map(response: response) + } +} diff --git a/Networking/NetworkingTests/Responses/jetpack-connection-url.json b/Networking/NetworkingTests/Responses/jetpack-connection-url.json index 46558f03931..3fe7dd79cd0 100644 --- a/Networking/NetworkingTests/Responses/jetpack-connection-url.json +++ b/Networking/NetworkingTests/Responses/jetpack-connection-url.json @@ -1 +1 @@ -"https://jetpack.wordpress.com/jetpack.authorize/1/?response_type=code&client_id=209946457&redirect_uri=https%3A%2F%2Ftest.jurassic.ninja%2Fwp-admin%2Fadmin.php%3Fhandler%3Djetpack-connection-webhooks%26action%3Dauthorize%26_wpnonce%3D905fcec%26redirect%3Dhttps%253A%252F%252Ftest.jurassic.ninja%252Fwp-admin%252Fadmin.php%253Fpage%253Djetpack&state=2&scope=editor%3A90c774bc742a8e35288b4aeaf259d0d0&user_email=test%40gmail.com&user_login=test&is_active=1&jp_version=11.2&auth_type=calypso&secret=hTtWoXu0eMS5Edl7aM6k99QuzkLWjCa0&blogname=Test&site_url=https%3A%2F%2Ftest.jurassic.ninja&home_url=https%3A%2F%2Ftest.jurassic.ninja&site_icon&site_lang=en_US&site_created=2022-08-24+07%3A20%3A24&allow_site_connection&locale=en&_ui=jetpack%3AFQfiXtAi%2Bc4QuCgXpeZju4ND&_ut=anon&_as=wp-cli" +"https://jetpack.wordpress.com/jetpack.authorize/1/?response_type=code&client_id=2099457" From 0eeedb91013ad05cc0bb3da606e1da77aff22c40 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 30 Aug 2022 15:10:41 +0700 Subject: [PATCH 013/256] Add tests for JetpackConnectionRemote --- .../Networking.xcodeproj/project.pbxproj | 4 ++ .../Remote/JetpackConnectionRemoteTests.swift | 56 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 Networking/NetworkingTests/Remote/JetpackConnectionRemoteTests.swift diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index 6a233890dd6..debc32de86f 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -630,6 +630,7 @@ DE34051928BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34051828BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift */; }; DE34051B28BDF12C00CF0D97 /* jetpack-connection-url.json in Resources */ = {isa = PBXBuildFile; fileRef = DE34051A28BDF12C00CF0D97 /* jetpack-connection-url.json */; }; DE34051D28BDF1C900CF0D97 /* JetpackConnectionURLMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34051C28BDF1C900CF0D97 /* JetpackConnectionURLMapperTests.swift */; }; + DE34051F28BDFB0B00CF0D97 /* JetpackConnectionRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34051E28BDFB0B00CF0D97 /* JetpackConnectionRemoteTests.swift */; }; DE5CA111288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json in Resources */ = {isa = PBXBuildFile; fileRef = DE5CA110288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json */; }; DE6F308727966FEF004E1C9A /* CouponReportListMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6F308627966FEF004E1C9A /* CouponReportListMapperTests.swift */; }; DE74F29A27E08F5A0002FE59 /* SiteSettingMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE74F29927E08F5A0002FE59 /* SiteSettingMapper.swift */; }; @@ -1317,6 +1318,7 @@ DE34051828BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackConnectionURLMapper.swift; sourceTree = ""; }; DE34051A28BDF12C00CF0D97 /* jetpack-connection-url.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "jetpack-connection-url.json"; sourceTree = ""; }; DE34051C28BDF1C900CF0D97 /* JetpackConnectionURLMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackConnectionURLMapperTests.swift; sourceTree = ""; }; + DE34051E28BDFB0B00CF0D97 /* JetpackConnectionRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackConnectionRemoteTests.swift; sourceTree = ""; }; DE5CA110288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-malformed-variations-and-image-alt.json"; sourceTree = ""; }; DE6F308627966FEF004E1C9A /* CouponReportListMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponReportListMapperTests.swift; sourceTree = ""; }; DE74F29927E08F5A0002FE59 /* SiteSettingMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSettingMapper.swift; sourceTree = ""; }; @@ -1599,6 +1601,7 @@ 31A451BC2786344B00FE81AA /* StripeRemoteTests.swift */, FE28F6EB268436C9004465C7 /* UserRemoteTests.swift */, 077F39D926A58ED700ABEADC /* SystemStatusRemoteTests.swift */, + DE34051E28BDFB0B00CF0D97 /* JetpackConnectionRemoteTests.swift */, ); path = Remote; sourceTree = ""; @@ -3111,6 +3114,7 @@ 0359EA1F27AAE4680048DE2D /* WCPayChargeMapperTests.swift in Sources */, 31D27C952602B737002EDB1D /* SitePluginsRemoteTests.swift in Sources */, DEC51AFB2769C66B009F3DF4 /* SystemStatusMapperTests.swift in Sources */, + DE34051F28BDFB0B00CF0D97 /* JetpackConnectionRemoteTests.swift in Sources */, 74AB5B4D21AF354E00859C12 /* SiteAPIMapperTests.swift in Sources */, 93D8BC01226BC20600AD2EB3 /* AccountSettingsRemoteTests.swift in Sources */, 262E5AD5255ACD6F000B2416 /* PaymentGatewayListMapperTests.swift in Sources */, diff --git a/Networking/NetworkingTests/Remote/JetpackConnectionRemoteTests.swift b/Networking/NetworkingTests/Remote/JetpackConnectionRemoteTests.swift new file mode 100644 index 00000000000..997406c58a5 --- /dev/null +++ b/Networking/NetworkingTests/Remote/JetpackConnectionRemoteTests.swift @@ -0,0 +1,56 @@ +import XCTest +@testable import Networking + +final class JetpackConnectionRemoteTests: XCTestCase { + + /// Dummy Network Wrapper + /// + let network = MockNetwork() + + /// Repeat always! + /// + override func setUp() { + network.removeAllSimulatedResponses() + } + + func test_fetchJetpackConnectionURL_correctly_returns_parsed_url() throws { + // Given + let siteURL = "http://test.com" + let remote = JetpackConnectionRemote(siteURL: siteURL, network: network) + let urlSuffix = "/jetpack/v4/connection/url" + network.simulateResponse(requestUrlSuffix: urlSuffix, filename: "jetpack-connection-url") + + // When + let result: Result = waitFor { promise in + remote.fetchJetpackConnectionURL { result in + promise(result) + } + } + + // Then + XCTAssertTrue(result.isSuccess) + let url = try XCTUnwrap(result.get()) + let expectedURL = "https://jetpack.wordpress.com/jetpack.authorize/1/?response_type=code&client_id=2099457" + assertEqual(url.absoluteString, expectedURL) + } + + func test_fetchJetpackConnectionURL_properly_relays_errors() { + // Given + let siteURL = "http://test.com" + let remote = JetpackConnectionRemote(siteURL: siteURL, network: network) + let urlSuffix = "/jetpack/v4/connection/url" + let error = NetworkError.unacceptableStatusCode(statusCode: 500) + network.simulateError(requestUrlSuffix: urlSuffix, error: error) + + // When + let result: Result = waitFor { promise in + remote.fetchJetpackConnectionURL { result in + promise(result) + } + } + + // Then + XCTAssertTrue(result.isFailure) + XCTAssertEqual(result.failure as? NetworkError, error) + } +} From dd516acafac06501fdd24ba5f05dd70f66dff34e Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 30 Aug 2022 15:33:33 +0700 Subject: [PATCH 014/256] Add tests for WordPressOrgRequest --- .../Networking.xcodeproj/project.pbxproj | 4 +++ .../Requests/WordPressOrgRequestTests.swift | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 Networking/NetworkingTests/Requests/WordPressOrgRequestTests.swift diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index debc32de86f..37e78eeb182 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -631,6 +631,7 @@ DE34051B28BDF12C00CF0D97 /* jetpack-connection-url.json in Resources */ = {isa = PBXBuildFile; fileRef = DE34051A28BDF12C00CF0D97 /* jetpack-connection-url.json */; }; DE34051D28BDF1C900CF0D97 /* JetpackConnectionURLMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34051C28BDF1C900CF0D97 /* JetpackConnectionURLMapperTests.swift */; }; DE34051F28BDFB0B00CF0D97 /* JetpackConnectionRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34051E28BDFB0B00CF0D97 /* JetpackConnectionRemoteTests.swift */; }; + DE34052128BDFE3500CF0D97 /* WordPressOrgRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE34052028BDFE3500CF0D97 /* WordPressOrgRequestTests.swift */; }; DE5CA111288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json in Resources */ = {isa = PBXBuildFile; fileRef = DE5CA110288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json */; }; DE6F308727966FEF004E1C9A /* CouponReportListMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6F308627966FEF004E1C9A /* CouponReportListMapperTests.swift */; }; DE74F29A27E08F5A0002FE59 /* SiteSettingMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE74F29927E08F5A0002FE59 /* SiteSettingMapper.swift */; }; @@ -1319,6 +1320,7 @@ DE34051A28BDF12C00CF0D97 /* jetpack-connection-url.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "jetpack-connection-url.json"; sourceTree = ""; }; DE34051C28BDF1C900CF0D97 /* JetpackConnectionURLMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackConnectionURLMapperTests.swift; sourceTree = ""; }; DE34051E28BDFB0B00CF0D97 /* JetpackConnectionRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackConnectionRemoteTests.swift; sourceTree = ""; }; + DE34052028BDFE3500CF0D97 /* WordPressOrgRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressOrgRequestTests.swift; sourceTree = ""; }; DE5CA110288A3E080077BEF9 /* product-malformed-variations-and-image-alt.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-malformed-variations-and-image-alt.json"; sourceTree = ""; }; DE6F308627966FEF004E1C9A /* CouponReportListMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponReportListMapperTests.swift; sourceTree = ""; }; DE74F29927E08F5A0002FE59 /* SiteSettingMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSettingMapper.swift; sourceTree = ""; }; @@ -1619,6 +1621,7 @@ isa = PBXGroup; children = ( B567AF2C20A0FB8F00AB6C62 /* AuthenticatedRequestTests.swift */, + DE34052028BDFE3500CF0D97 /* WordPressOrgRequestTests.swift */, B567AF2D20A0FB8F00AB6C62 /* DotcomRequestTests.swift */, B567AF2E20A0FB8F00AB6C62 /* JetpackRequestTests.swift */, ); @@ -3136,6 +3139,7 @@ 7412A8EE21B6E335005D182A /* ReportOrderMapperTests.swift in Sources */, AED8AEBC272A997500663FCC /* IgnoringResponseMapperTests.swift in Sources */, 74CF0A8C22414D7800DB993F /* ProductMapperTests.swift in Sources */, + DE34052128BDFE3500CF0D97 /* WordPressOrgRequestTests.swift in Sources */, 45152815257A83DD0076B03C /* ProductAttributesRemoteTests.swift in Sources */, B505F6D720BEE58800BB1B69 /* AccountRemoteTests.swift in Sources */, 453305EB2459E01A00264E50 /* PostMapperTests.swift in Sources */, diff --git a/Networking/NetworkingTests/Requests/WordPressOrgRequestTests.swift b/Networking/NetworkingTests/Requests/WordPressOrgRequestTests.swift new file mode 100644 index 00000000000..4a3248177e7 --- /dev/null +++ b/Networking/NetworkingTests/Requests/WordPressOrgRequestTests.swift @@ -0,0 +1,31 @@ +import XCTest +@testable import Networking + +final class WordPressOrgRequestTests: XCTestCase { + + private let baseURL = "http://test.com" + private let path = "/test/request" + + func test_request_url_is_correct() throws { + // Given + let request = WordPressOrgRequest(baseURL: baseURL, method: .get, path: path) + + // When + let url = try XCTUnwrap(request.asURLRequest().url) + + // Then + let expectedURL = "http://test.com/wp-json/test/request" + assertEqual(url.absoluteString, expectedURL) + } + + func test_request_method_is_correct() throws { + // Given + let request = WordPressOrgRequest(baseURL: baseURL, method: .get, path: path) + + // When + let urlRequest = try request.asURLRequest() + + // Then + assertEqual(urlRequest.httpMethod, "GET") + } +} From 140270afaeb1488724fcf3895234e9788dbdb670 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 30 Aug 2022 16:04:09 +0700 Subject: [PATCH 015/256] Update comment for WordPressOrgNetwork --- Networking/Networking/Network/WordPressOrgNetwork.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Networking/Networking/Network/WordPressOrgNetwork.swift b/Networking/Networking/Network/WordPressOrgNetwork.swift index 020ce7a6a14..4913cd69c5d 100644 --- a/Networking/Networking/Network/WordPressOrgNetwork.swift +++ b/Networking/Networking/Network/WordPressOrgNetwork.swift @@ -121,6 +121,8 @@ public final class WordPressOrgNetwork: Network { } private extension WordPressOrgNetwork { + /// Creates a session manager with injected user agent and authenticator for handling cookie-nonce/token + /// func makeSessionManager(configuration sessionConfiguration: URLSessionConfiguration) -> Alamofire.SessionManager { var additionalHeaders: [String: AnyObject] = [:] if let userAgent = self.userAgent { From a35ab76c26e2d85c3a1bd22c6085b11d21f7a4b6 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Sat, 27 Aug 2022 11:04:23 +0700 Subject: [PATCH 016/256] Add extension to WordPressOrgCredentials to create cookie nonce authenticator --- ...ordPressOrgCredentials+Authenticator.swift | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 WooCommerce/Classes/Authentication/WordPressOrgCredentials+Authenticator.swift diff --git a/WooCommerce/Classes/Authentication/WordPressOrgCredentials+Authenticator.swift b/WooCommerce/Classes/Authentication/WordPressOrgCredentials+Authenticator.swift new file mode 100644 index 00000000000..5ab47eafead --- /dev/null +++ b/WooCommerce/Classes/Authentication/WordPressOrgCredentials+Authenticator.swift @@ -0,0 +1,66 @@ +import Foundation +import WordPressKit +import WordPressAuthenticator + +/// Extension to create cookie nonce authenticator from WP.org credentials. +/// +extension WordPressOrgCredentials { + var loginURL: String { + let value = optionValue(for: Strings.loginURLKey) as? String + return value ?? siteURL + Strings.loginPath + } + + var adminURL: String { + let value = optionValue(for: Strings.adminURLKey) as? String + return value ?? siteURL + Strings.adminPath + } + + var version: String { + let value = optionValue(for: Strings.versionKey) + if let stringValue = value as? String { + return stringValue + } + + if let numberValue = value as? NSNumber { + return numberValue.stringValue + } + + return "" + } + + /// Returns a cookie nonce authenticator based on the current credentials + /// + func makeCookieNonceAuthenticator() -> CookieNonceAuthenticator? { + guard let loginURL = URL(string: loginURL), + let adminURL = URL(string: adminURL) else { + return nil + } + return CookieNonceAuthenticator(username: username, + password: password, + loginURL: loginURL, + adminURL: adminURL, + version: version) + } +} + +// MARK: - Private helpers +// +private extension WordPressOrgCredentials { + /// Returns value for an option given a key. + /// + func optionValue(for key: String) -> Any? { + let option = options[key] as? [String: Any] + return option?[Strings.valueKey] + } +} + +private extension WordPressOrgCredentials { + enum Strings { + static let loginPath = "/wp-login.php" + static let adminPath = "/wp-admin" + static let loginURLKey = "login_url" + static let adminURLKey = "admin_url" + static let versionKey = "software_version" + static let valueKey = "value" + } +} From 0abea98b63c91314edbf900a3d5bcceaeee4c596 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 31 Aug 2022 08:52:13 +0700 Subject: [PATCH 017/256] Update reference for WordPressOrgCredentials extension --- WooCommerce/WooCommerce.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index b6cdfd40301..7a0fd78ccda 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1648,6 +1648,7 @@ DE4B3B5826A7041800EEF2D8 /* EdgeInsets+Woo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4B3B5726A7041800EEF2D8 /* EdgeInsets+Woo.swift */; }; DE4D308928507B5B00E36ADD /* CouponCreationSuccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4D308828507B5B00E36ADD /* CouponCreationSuccess.swift */; }; DE4FB7732812AE96003D20D6 /* FilterListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4FB7722812AE96003D20D6 /* FilterListView.swift */; }; + DE50294928BEF4CF00551736 /* WordPressOrgCredentials+Authenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE50294828BEF4CF00551736 /* WordPressOrgCredentials+Authenticator.swift */; }; DE525499268C8B32007A5829 /* UIRefreshControl+Woo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE525498268C8B32007A5829 /* UIRefreshControl+Woo.swift */; }; DE61978B28991F0E005E4362 /* WKWebView+Authenticated.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE61978A28991F0E005E4362 /* WKWebView+Authenticated.swift */; }; DE61978D289A5326005E4362 /* WooSetupWebViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE61978C289A5326005E4362 /* WooSetupWebViewModelTests.swift */; }; @@ -3502,6 +3503,7 @@ DE4B3B5726A7041800EEF2D8 /* EdgeInsets+Woo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EdgeInsets+Woo.swift"; sourceTree = ""; }; DE4D308828507B5B00E36ADD /* CouponCreationSuccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponCreationSuccess.swift; sourceTree = ""; }; DE4FB7722812AE96003D20D6 /* FilterListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterListView.swift; sourceTree = ""; }; + DE50294828BEF4CF00551736 /* WordPressOrgCredentials+Authenticator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "WordPressOrgCredentials+Authenticator.swift"; path = "Classes/Authentication/WordPressOrgCredentials+Authenticator.swift"; sourceTree = SOURCE_ROOT; }; DE525498268C8B32007A5829 /* UIRefreshControl+Woo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIRefreshControl+Woo.swift"; sourceTree = ""; }; DE61978A28991F0E005E4362 /* WKWebView+Authenticated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WKWebView+Authenticated.swift"; sourceTree = ""; }; DE61978C289A5326005E4362 /* WooSetupWebViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooSetupWebViewModelTests.swift; sourceTree = ""; }; @@ -7332,6 +7334,7 @@ CE1CCB4C20572444000EE3AC /* Extensions */ = { isa = PBXGroup; children = ( + DE50294828BEF4CF00551736 /* WordPressOrgCredentials+Authenticator.swift */, B58B4ABF2108FF6100076FDD /* Array+Helpers.swift */, B59C09D82188CBB100AB41D6 /* Array+Notes.swift */, 45EF7983244F26BB00B22BA2 /* Array+IndexPath.swift */, @@ -9492,6 +9495,7 @@ B55D4C0620B6027200D7A50F /* AuthenticationManager.swift in Sources */, 451A04F02386F7B500E368C9 /* ProductImageCollectionViewCell.swift in Sources */, AEACCB6D2785FF4A000D01F0 /* NavigationRow.swift in Sources */, + DE50294928BEF4CF00551736 /* WordPressOrgCredentials+Authenticator.swift in Sources */, 02E8B17E23E2C8D900A43403 /* ProductImageActionHandler.swift in Sources */, 023D877925EC8BCB00625963 /* UIScrollView+LargeTitleWorkaround.swift in Sources */, 2664210326F40FB1001FC5B4 /* View+ScrollModifiers.swift in Sources */, From 540f4f052a000220dcf23e2c1af419998ad41720 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 31 Aug 2022 08:53:59 +0700 Subject: [PATCH 018/256] Add new view model for jetpack connection error --- .../JetpackConnectionErrorViewModel.swift | 110 ++++++++++++++++++ .../WooCommerce.xcodeproj/project.pbxproj | 4 + 2 files changed, 114 insertions(+) create mode 100644 WooCommerce/Classes/Authentication/Navigation Exceptions/JetpackConnectionErrorViewModel.swift diff --git a/WooCommerce/Classes/Authentication/Navigation Exceptions/JetpackConnectionErrorViewModel.swift b/WooCommerce/Classes/Authentication/Navigation Exceptions/JetpackConnectionErrorViewModel.swift new file mode 100644 index 00000000000..60940160785 --- /dev/null +++ b/WooCommerce/Classes/Authentication/Navigation Exceptions/JetpackConnectionErrorViewModel.swift @@ -0,0 +1,110 @@ +import Foundation +import WordPressAuthenticator + +final class JetpackConnectionErrorViewModel: ULErrorViewModel { + private let siteURL: String + private var jetpackConnectionURL: URL? + + init(siteURL: String, credentials: WordPressOrgCredentials) { + self.siteURL = siteURL + fetchJetpackConnectionURL(with: credentials) + } + + // MARK: - Data and configuration + + let image: UIImage = .productErrorImage + + var text: NSAttributedString { + let font: UIFont = .body + let boldFont: UIFont = font.bold + + let boldSiteAddress = NSAttributedString(string: siteURL.trimHTTPScheme(), + attributes: [.font: boldFont]) + let attributedString = NSMutableAttributedString(string: Localization.noJetpackEmail) + attributedString.replaceFirstOccurrence(of: "%@", with: boldSiteAddress) + + return attributedString + } + + let isAuxiliaryButtonHidden = true + + let auxiliaryButtonTitle = "" + + let primaryButtonTitle = Localization.primaryButtonTitle + + let secondaryButtonTitle = Localization.secondaryButtonTitle + + func viewDidLoad(_ viewController: UIViewController?) { + // no-op + } + + func didTapPrimaryButton(in viewController: UIViewController?) { + showJetpackConnectionWebView(from: viewController) + } + + func didTapSecondaryButton(in viewController: UIViewController?) { + viewController?.navigationController?.popToRootViewController(animated: true) + } + + func didTapAuxiliaryButton(in viewController: UIViewController?) { + // no-op + } +} + +// MARK: - Private helpers +private extension JetpackConnectionErrorViewModel { + func showJetpackConnectionWebView(from viewController: UIViewController?) { + guard let url = jetpackConnectionURL, + let viewController = viewController else { + return + } + WebviewHelper.launch(url, with: viewController) + } + + func fetchJetpackConnectionURL(with credentials: WordPressOrgCredentials) { + guard let api = WordPressOrgAPI(credentials: credentials) else { + return + } + + Task { [weak self] in + guard let self = self else { return } + let data = try? await api.request(method: .get, path: Constants.jetpackConnectionFetchPath, parameters: nil) + await MainActor.run { [weak self] in + guard let self = self else { return } + if let data = data, let escapedString = String(data: data, encoding: .utf8) { + let urlString = escapedString + .replacingOccurrences(of: "\"", with: "") + .replacingOccurrences(of: "\\", with: "") + self.jetpackConnectionURL = URL(string: urlString) + } + } + } + + } +} + +private extension JetpackConnectionErrorViewModel { + enum Localization { + static let noJetpackEmail = NSLocalizedString( + "It looks like your account is not connected to %@'s Jetpack", + + comment: "Message explaining that the entered site credentials belong to an account that is not connected to the site's Jetpack. " + + + "Reads like 'It looks like your account is not connected to awebsite.com's Jetpack") + + static let primaryButtonTitle = NSLocalizedString( + "Connect Jetpack to your account", + comment: "Button linking to web view for setting up Jetpack connection. " + + "Presented when logging in with store credentials of an account not connected to the site's Jetpack") + + static let secondaryButtonTitle = NSLocalizedString( + "Log In With Another Account", + comment: "Action button that will restart the login flow." + + "Presented when logging in with store credentials of an account not connected to the site's Jetpack" + ) + } + + enum Constants { + static let jetpackConnectionFetchPath = "/jetpack/v4/connection/url" + } +} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 7a0fd78ccda..2f405224034 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1649,6 +1649,7 @@ DE4D308928507B5B00E36ADD /* CouponCreationSuccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4D308828507B5B00E36ADD /* CouponCreationSuccess.swift */; }; DE4FB7732812AE96003D20D6 /* FilterListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4FB7722812AE96003D20D6 /* FilterListView.swift */; }; DE50294928BEF4CF00551736 /* WordPressOrgCredentials+Authenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE50294828BEF4CF00551736 /* WordPressOrgCredentials+Authenticator.swift */; }; + DE50294B28BEF6B100551736 /* JetpackConnectionErrorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE50294A28BEF6B100551736 /* JetpackConnectionErrorViewModel.swift */; }; DE525499268C8B32007A5829 /* UIRefreshControl+Woo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE525498268C8B32007A5829 /* UIRefreshControl+Woo.swift */; }; DE61978B28991F0E005E4362 /* WKWebView+Authenticated.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE61978A28991F0E005E4362 /* WKWebView+Authenticated.swift */; }; DE61978D289A5326005E4362 /* WooSetupWebViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE61978C289A5326005E4362 /* WooSetupWebViewModelTests.swift */; }; @@ -3504,6 +3505,7 @@ DE4D308828507B5B00E36ADD /* CouponCreationSuccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponCreationSuccess.swift; sourceTree = ""; }; DE4FB7722812AE96003D20D6 /* FilterListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterListView.swift; sourceTree = ""; }; DE50294828BEF4CF00551736 /* WordPressOrgCredentials+Authenticator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "WordPressOrgCredentials+Authenticator.swift"; path = "Classes/Authentication/WordPressOrgCredentials+Authenticator.swift"; sourceTree = SOURCE_ROOT; }; + DE50294A28BEF6B100551736 /* JetpackConnectionErrorViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JetpackConnectionErrorViewModel.swift; sourceTree = ""; }; DE525498268C8B32007A5829 /* UIRefreshControl+Woo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIRefreshControl+Woo.swift"; sourceTree = ""; }; DE61978A28991F0E005E4362 /* WKWebView+Authenticated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WKWebView+Authenticated.swift"; sourceTree = ""; }; DE61978C289A5326005E4362 /* WooSetupWebViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooSetupWebViewModelTests.swift; sourceTree = ""; }; @@ -8017,6 +8019,7 @@ DEFA3D922897D8930076FAE1 /* NoWooErrorViewModel.swift */, DEFB3010289904E300A620B3 /* WooSetupWebViewModel.swift */, DE3404E728B4B96800CF0D97 /* NonAtomicSiteViewModel.swift */, + DE50294A28BEF6B100551736 /* JetpackConnectionErrorViewModel.swift */, ); path = "Navigation Exceptions"; sourceTree = ""; @@ -9551,6 +9554,7 @@ 45E9A6E424DAE1EA00A600E8 /* ProductReviewsViewController.swift in Sources */, D8736B7522F1FE1600A14A29 /* BadgeLabel.swift in Sources */, B5F8B7E02194759100DAB7E2 /* ReviewDetailsViewController.swift in Sources */, + DE50294B28BEF6B100551736 /* JetpackConnectionErrorViewModel.swift in Sources */, 262A098B2628C51D0033AD20 /* OrderAddOnListViewModel.swift in Sources */, 0262DA5323A238460029AF30 /* UnitInputTableViewCell.swift in Sources */, D8EE9692264D328A0033B2F9 /* ReceiptViewController.swift in Sources */, From bd00d206934c797df970156128ba3636f62e63a0 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Sat, 27 Aug 2022 12:00:39 +0700 Subject: [PATCH 019/256] Show jetpack connection screen from the login flow --- .../AuthenticationManager.swift | 19 ++++++++++++++++--- .../ServiceLocator/Authentication.swift | 1 + .../Classes/ViewRelated/AppCoordinator.swift | 1 + 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/WooCommerce/Classes/Authentication/AuthenticationManager.swift b/WooCommerce/Classes/Authentication/AuthenticationManager.swift index 9197935f24e..abb8071b1b3 100644 --- a/WooCommerce/Classes/Authentication/AuthenticationManager.swift +++ b/WooCommerce/Classes/Authentication/AuthenticationManager.swift @@ -188,13 +188,18 @@ class AuthenticationManager: Authentication { /// and returns an error view controller if not. func errorViewController(for siteURL: String, with matcher: ULAccountMatcher, + credentials: AuthenticatorCredentials? = nil, navigationController: UINavigationController, onStorePickerDismiss: @escaping () -> Void) -> UIViewController? { /// Account mismatched case guard matcher.match(originalURL: siteURL) else { + if let credentials = credentials?.wporg { + DDLogWarn("⚠️ Present Jetpack connection error for site: \(String(describing: siteURL))") + return jetpackConnectionUI(for: siteURL, with: credentials) + } DDLogWarn("⚠️ Present account mismatch error for site: \(String(describing: siteURL))") - return accountMismatchUI(for: siteURL, with: matcher) + return accountMismatchUI(for: siteURL, with: matcher, credentials: credentials) } /// No Woo found @@ -349,7 +354,7 @@ extension AuthenticationManager: WordPressAuthenticatorDelegate { let matcher = ULAccountMatcher(storageManager: storageManager) matcher.refreshStoredSites() - if let vc = errorViewController(for: siteURL, with: matcher, navigationController: navigationController, onStorePickerDismiss: onDismiss) { + if let vc = errorViewController(for: siteURL, with: matcher, credentials: credentials, navigationController: navigationController, onStorePickerDismiss: onDismiss) { loggedOutAppSettings?.setErrorLoginSiteAddress(siteURL) navigationController.show(vc, sender: nil) } else { @@ -594,10 +599,18 @@ private extension AuthenticationManager { return ULErrorViewController(viewModel: viewModel) } + /// The error screen to be displayed when the user tries to enter as site + /// whose Jetpack is not connected to their WP.com account. + /// + func jetpackConnectionUI(for siteURL: String, with credentials: WordPressOrgCredentials) -> UIViewController { + let viewModel = JetpackConnectionErrorViewModel(siteURL: siteURL, credentials: credentials) + return ULErrorViewController(viewModel: viewModel) + } + /// The error screen to be displayed when the user tries to enter a site /// whose Jetpack is not associated with their account. /// - func accountMismatchUI(for siteURL: String, with matcher: ULAccountMatcher) -> UIViewController { + func accountMismatchUI(for siteURL: String, with matcher: ULAccountMatcher, credentials: AuthenticatorCredentials? = nil) -> UIViewController { let viewModel = WrongAccountErrorViewModel(siteURL: siteURL, showsConnectedStores: matcher.hasConnectedStores) let mismatchAccountUI = ULAccountMismatchViewController(viewModel: viewModel) return mismatchAccountUI diff --git a/WooCommerce/Classes/ServiceLocator/Authentication.swift b/WooCommerce/Classes/ServiceLocator/Authentication.swift index 557294a2efb..b97c0f4ce4b 100644 --- a/WooCommerce/Classes/ServiceLocator/Authentication.swift +++ b/WooCommerce/Classes/ServiceLocator/Authentication.swift @@ -31,6 +31,7 @@ protocol Authentication { /// func errorViewController(for siteURL: String, with matcher: ULAccountMatcher, + credentials: AuthenticatorCredentials?, navigationController: UINavigationController, onStorePickerDismiss: @escaping () -> Void) -> UIViewController? } diff --git a/WooCommerce/Classes/ViewRelated/AppCoordinator.swift b/WooCommerce/Classes/ViewRelated/AppCoordinator.swift index 142d6ec0b1e..2fc8bfcb1cc 100644 --- a/WooCommerce/Classes/ViewRelated/AppCoordinator.swift +++ b/WooCommerce/Classes/ViewRelated/AppCoordinator.swift @@ -226,6 +226,7 @@ private extension AppCoordinator { if let authenticationUI = authenticationManager.authenticationUI() as? UINavigationController, let errorController = authenticationManager.errorViewController(for: siteURL, with: matcher, + credentials: nil, navigationController: authenticationUI, onStorePickerDismiss: {}) { window.rootViewController = authenticationUI From 66d29319c625958ab99ecc7a0162852445b03d45 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 31 Aug 2022 09:01:35 +0700 Subject: [PATCH 020/256] Update JetpackConnectionErrorViewModel to dispatch action --- .../JetpackConnectionErrorViewModel.swift | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/WooCommerce/Classes/Authentication/Navigation Exceptions/JetpackConnectionErrorViewModel.swift b/WooCommerce/Classes/Authentication/Navigation Exceptions/JetpackConnectionErrorViewModel.swift index 60940160785..b5405825844 100644 --- a/WooCommerce/Classes/Authentication/Navigation Exceptions/JetpackConnectionErrorViewModel.swift +++ b/WooCommerce/Classes/Authentication/Navigation Exceptions/JetpackConnectionErrorViewModel.swift @@ -1,12 +1,15 @@ import Foundation +import Yosemite import WordPressAuthenticator final class JetpackConnectionErrorViewModel: ULErrorViewModel { private let siteURL: String private var jetpackConnectionURL: URL? + private let stores: StoresManager - init(siteURL: String, credentials: WordPressOrgCredentials) { + init(siteURL: String, credentials: WordPressOrgCredentials, stores: StoresManager = ServiceLocator.stores) { self.siteURL = siteURL + self.stores = stores fetchJetpackConnectionURL(with: credentials) } @@ -62,24 +65,21 @@ private extension JetpackConnectionErrorViewModel { } func fetchJetpackConnectionURL(with credentials: WordPressOrgCredentials) { - guard let api = WordPressOrgAPI(credentials: credentials) else { + guard let authenticator = credentials.makeCookieNonceAuthenticator() else { return } - - Task { [weak self] in + let action = JetpackConnectionAction.fetchJetpackConnectionURL(siteURL: credentials.siteURL, + authenticator: authenticator, + completion: { [weak self] result in guard let self = self else { return } - let data = try? await api.request(method: .get, path: Constants.jetpackConnectionFetchPath, parameters: nil) - await MainActor.run { [weak self] in - guard let self = self else { return } - if let data = data, let escapedString = String(data: data, encoding: .utf8) { - let urlString = escapedString - .replacingOccurrences(of: "\"", with: "") - .replacingOccurrences(of: "\\", with: "") - self.jetpackConnectionURL = URL(string: urlString) - } + switch result { + case .success(let url): + self.jetpackConnectionURL = url + case .failure(let error): + DDLogWarn("⚠️ Error fetching Jetpack connection URL: \(error)") } - } - + }) + stores.dispatch(action) } } @@ -87,9 +87,7 @@ private extension JetpackConnectionErrorViewModel { enum Localization { static let noJetpackEmail = NSLocalizedString( "It looks like your account is not connected to %@'s Jetpack", - comment: "Message explaining that the entered site credentials belong to an account that is not connected to the site's Jetpack. " - + "Reads like 'It looks like your account is not connected to awebsite.com's Jetpack") static let primaryButtonTitle = NSLocalizedString( @@ -103,8 +101,4 @@ private extension JetpackConnectionErrorViewModel { "Presented when logging in with store credentials of an account not connected to the site's Jetpack" ) } - - enum Constants { - static let jetpackConnectionFetchPath = "/jetpack/v4/connection/url" - } } From c7957310d29e3388cd89ddc6b1450d5ba9bea717 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Mon, 29 Aug 2022 11:43:13 +0700 Subject: [PATCH 021/256] Add new variable for loading state of the primary button --- .../Navigation Exceptions/ULErrorViewModel.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/WooCommerce/Classes/Authentication/Navigation Exceptions/ULErrorViewModel.swift b/WooCommerce/Classes/Authentication/Navigation Exceptions/ULErrorViewModel.swift index 33e6b58f100..5e18ff0d77a 100644 --- a/WooCommerce/Classes/Authentication/Navigation Exceptions/ULErrorViewModel.swift +++ b/WooCommerce/Classes/Authentication/Navigation Exceptions/ULErrorViewModel.swift @@ -1,4 +1,5 @@ import UIKit +import Combine /// Abstracts different configurations and logic related to user interaction /// for error view controllers presented as part of the Unified Login flow @@ -24,6 +25,9 @@ protocol ULErrorViewModel { /// Indicates whether the primary button is visible var isPrimaryButtonHidden: Bool { get } + /// Indicates whether the primary button is showing the loading indicator + var isPrimaryButtonLoading: AnyPublisher { get } + /// Provides a title for a secondary action button var secondaryButtonTitle: String { get } @@ -56,6 +60,8 @@ extension ULErrorViewModel { var isPrimaryButtonHidden: Bool { false } + var isPrimaryButtonLoading: AnyPublisher { Just(false).eraseToAnyPublisher() } + var isSecondaryButtonHidden: Bool { false } var auxiliaryView: UIView? { nil } From b6fba2f0a9a4eb6396225b291ce06f0bb1ddb34d Mon Sep 17 00:00:00 2001 From: Huong Do Date: Mon, 29 Aug 2022 11:43:26 +0700 Subject: [PATCH 022/256] Update the primary button to be animatable --- .../ULErrorViewController.swift | 13 +++++++++++-- .../Navigation Exceptions/ULErrorViewController.xib | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/WooCommerce/Classes/Authentication/Navigation Exceptions/ULErrorViewController.swift b/WooCommerce/Classes/Authentication/Navigation Exceptions/ULErrorViewController.swift index b37c47fb009..2dabddb1b60 100644 --- a/WooCommerce/Classes/Authentication/Navigation Exceptions/ULErrorViewController.swift +++ b/WooCommerce/Classes/Authentication/Navigation Exceptions/ULErrorViewController.swift @@ -1,3 +1,4 @@ +import Combine import UIKit import WordPressAuthenticator import SafariServices @@ -13,7 +14,7 @@ final class ULErrorViewController: UIViewController { /// Contains a vertical stack of the image, error message, and extra info button by default. @IBOutlet private weak var contentStackView: UIStackView! - @IBOutlet private weak var primaryButton: UIButton! + @IBOutlet private weak var primaryButton: NUXButton! @IBOutlet private weak var secondaryButton: UIButton! @IBOutlet private weak var imageView: UIImageView! @IBOutlet private weak var errorMessage: UILabel! @@ -27,6 +28,8 @@ final class ULErrorViewController: UIViewController { @IBOutlet private weak var stackViewLeadingConstraint: NSLayoutConstraint! @IBOutlet private weak var stackViewTrailingConstraint: NSLayoutConstraint! + private var primaryButtonSubscription: AnyCancellable? + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { UIDevice.isPad() ? .all : .portrait } @@ -110,12 +113,18 @@ private extension ULErrorViewController { } func configurePrimaryButton() { - primaryButton.applyPrimaryButtonStyle() + primaryButton.isPrimary = true primaryButton.isHidden = viewModel.isPrimaryButtonHidden primaryButton.setTitle(viewModel.primaryButtonTitle, for: .normal) primaryButton.on(.touchUpInside) { [weak self] _ in self?.didTapPrimaryButton() } + + primaryButtonSubscription = viewModel.isPrimaryButtonLoading.sink { [weak self] isLoading in + guard let self = self else { return } + self.primaryButton.isEnabled = !isLoading + self.primaryButton.showActivityIndicator(isLoading) + } } func configureSecondaryButton() { diff --git a/WooCommerce/Classes/Authentication/Navigation Exceptions/ULErrorViewController.xib b/WooCommerce/Classes/Authentication/Navigation Exceptions/ULErrorViewController.xib index 19c666b9475..f0beac57e76 100644 --- a/WooCommerce/Classes/Authentication/Navigation Exceptions/ULErrorViewController.xib +++ b/WooCommerce/Classes/Authentication/Navigation Exceptions/ULErrorViewController.xib @@ -74,7 +74,7 @@ -