From 762da86c08cae3b9c0fe7c48a54331e188623357 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 14 Dec 2022 08:03:00 +0530 Subject: [PATCH 01/15] Import Keychain access into Networking layer to store application password. --- Podfile | 3 +++ Podfile.lock | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Podfile b/Podfile index 5d3266f1453..c3770c3c794 100644 --- a/Podfile +++ b/Podfile @@ -177,6 +177,9 @@ def networking_pods # Used for HTML parsing aztec + # Used for storing application password + keychain + wordpress_kit end diff --git a/Podfile.lock b/Podfile.lock index 593fe5738ab..7153ab271aa 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -169,6 +169,6 @@ SPEC CHECKSUMS: ZendeskSupportProvidersSDK: 2bdf8544f7cd0fd4c002546f5704b813845beb2a ZendeskSupportSDK: 3a8e508ab1d9dd22dc038df6c694466414e037ba -PODFILE CHECKSUM: cc3c6d9d046f232dba2b1be5dad549543da973bd +PODFILE CHECKSUM: 04b17163281ec1c5967ef28a56f130d10c73bce9 COCOAPODS: 1.11.3 From 8306481a2df869d9e40c0a36cb682f2850ba1303 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 14 Dec 2022 08:05:04 +0530 Subject: [PATCH 02/15] Add `ApplicationPasswordNetwork` to perform application password generation and deletion network actions. --- .../Networking.xcodeproj/project.pbxproj | 16 ++++--- .../Network/ApplicationPasswordNetwork.swift | 47 +++++++++++++++++++ 2 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 Networking/Networking/Network/ApplicationPasswordNetwork.swift diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index a73041e96e8..2989375eba6 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -729,7 +729,8 @@ E1BAB2C32913FA6400C3982B /* ResponseDataValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BAB2C22913FA6400C3982B /* ResponseDataValidator.swift */; }; E1BAB2C52913FB1800C3982B /* WordPressApiValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BAB2C42913FB1800C3982B /* WordPressApiValidator.swift */; }; E1BAB2C72913FB5800C3982B /* WordPressApiError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BAB2C62913FB5800C3982B /* WordPressApiError.swift */; }; - EE54C8942947229800A9BF61 /* ApplicationPasswordUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE54C8932947229800A9BF61 /* ApplicationPasswordUseCase.swift */; }; + EE54C89F2947782E00A9BF61 /* ApplicationPasswordUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE54C89E2947782E00A9BF61 /* ApplicationPasswordUseCase.swift */; }; + EE54C8A5294859D200A9BF61 /* ApplicationPasswordNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE54C8A4294859D200A9BF61 /* ApplicationPasswordNetwork.swift */; }; EE8A86F1286C5226003E8AA4 /* media-update-product-id-in-wordpress-site.json in Resources */ = {isa = PBXBuildFile; fileRef = EE8A86F0286C5226003E8AA4 /* media-update-product-id-in-wordpress-site.json */; }; EECB7EE8286555180028C888 /* media-update-product-id.json in Resources */ = {isa = PBXBuildFile; fileRef = EECB7EE7286555180028C888 /* media-update-product-id.json */; }; FE28F6E226840DED004465C7 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE28F6E126840DED004465C7 /* User.swift */; }; @@ -1491,7 +1492,8 @@ E1BAB2C22913FA6400C3982B /* ResponseDataValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseDataValidator.swift; sourceTree = ""; }; E1BAB2C42913FB1800C3982B /* WordPressApiValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressApiValidator.swift; sourceTree = ""; }; E1BAB2C62913FB5800C3982B /* WordPressApiError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressApiError.swift; sourceTree = ""; }; - EE54C8932947229800A9BF61 /* ApplicationPasswordUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordUseCase.swift; sourceTree = ""; }; + EE54C89E2947782E00A9BF61 /* ApplicationPasswordUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordUseCase.swift; sourceTree = ""; }; + EE54C8A4294859D200A9BF61 /* ApplicationPasswordNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordNetwork.swift; sourceTree = ""; }; EE8A86F0286C5226003E8AA4 /* media-update-product-id-in-wordpress-site.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "media-update-product-id-in-wordpress-site.json"; sourceTree = ""; }; EECB7EE7286555180028C888 /* media-update-product-id.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "media-update-product-id.json"; sourceTree = ""; }; F3F25DC15EC1D7C631169CB5 /* Pods_Networking.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Networking.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1704,6 +1706,7 @@ B518662320A099BF00037A38 /* AlamofireNetwork.swift */, B518662620A09BCC00037A38 /* MockNetwork.swift */, D87F6150226591E10031A13B /* NullNetwork.swift */, + EE54C8A4294859D200A9BF61 /* ApplicationPasswordNetwork.swift */, ); path = Network; sourceTree = ""; @@ -1816,7 +1819,7 @@ B557D9E5209753AA005962F4 /* Networking */ = { isa = PBXGroup; children = ( - EE54C8922947227900A9BF61 /* ApplicationPassword */, + EE54C899294777D000A9BF61 /* ApplicationPassword */, B5A0369F214C0F4C00774E2C /* Internal */, B5BB1D0A20A204F400112D92 /* Extensions */, B567AF2720A0FA0A00AB6C62 /* Mapper */, @@ -2578,10 +2581,10 @@ path = SystemStatusDetails; sourceTree = ""; }; - EE54C8922947227900A9BF61 /* ApplicationPassword */ = { + EE54C899294777D000A9BF61 /* ApplicationPassword */ = { isa = PBXGroup; children = ( - EE54C8932947229800A9BF61 /* ApplicationPasswordUseCase.swift */, + EE54C89E2947782E00A9BF61 /* ApplicationPasswordUseCase.swift */, ); path = ApplicationPassword; sourceTree = ""; @@ -3072,7 +3075,7 @@ 743E84EE2217244C00FAC9D7 /* ShipmentTrackingListMapper.swift in Sources */, 451A97E5260B631E0059D135 /* ShippingLabelPredefinedPackage.swift in Sources */, BAB373722795A1FB00837B4A /* OrderTaxLine.swift in Sources */, - EE54C8942947229800A9BF61 /* ApplicationPasswordUseCase.swift in Sources */, + EE54C89F2947782E00A9BF61 /* ApplicationPasswordUseCase.swift in Sources */, B567AF2520A0CCA300AB6C62 /* AuthenticatedRequest.swift in Sources */, 453305E92459DF2100264E50 /* PostMapper.swift in Sources */, E12552C526385B05001CEE70 /* ShippingLabelAddressValidationSuccess.swift in Sources */, @@ -3222,6 +3225,7 @@ 93D8BBFD226BBEE800AD2EB3 /* AccountSettingsMapper.swift in Sources */, 31D27C8726028CE9002EDB1D /* SitePluginsMapper.swift in Sources */, 74D522B62113607F00042831 /* StatGranularity.swift in Sources */, + EE54C8A5294859D200A9BF61 /* ApplicationPasswordNetwork.swift in Sources */, 02C2549A25636E1500A04423 /* ShippingLabelAddress.swift in Sources */, 03DCB786262739D200C8953D /* CouponMapper.swift in Sources */, B518662220A097C200037A38 /* Network.swift in Sources */, diff --git a/Networking/Networking/Network/ApplicationPasswordNetwork.swift b/Networking/Networking/Network/ApplicationPasswordNetwork.swift new file mode 100644 index 00000000000..cbebc9beeba --- /dev/null +++ b/Networking/Networking/Network/ApplicationPasswordNetwork.swift @@ -0,0 +1,47 @@ +import Combine +import Foundation +import Alamofire + +public class ApplicationPasswordNetwork: Network { + /// WordPress.com Credentials. + /// + private let credentials: Credentials + + public var session: URLSession { SessionManager.default.session } + + /// Public Initializer + /// + public required init(credentials: Credentials) { + self.credentials = credentials + } + + /// Executes the specified Network Request. Upon completion, the payload will be sent back to the caller as a Data instance. + /// + /// - Important: + /// - Authentication Headers will be injected, based on the Network's Credentials. + /// + /// - 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) { + let request = AuthenticatedRequest(credentials: credentials, request: request) + + Alamofire.request(request).responseData { response in + completion(response.result.toSwiftResult()) + } + } + + @available(*, deprecated, message: "Not implemented. Use the `Result` based method instead.") + public func responseData(for request: URLRequestConvertible, completion: @escaping (Data?, Error?) -> Void) { } + + @available(*, deprecated, message: "Not implemented. Use the `Result` based method instead.") + public func responseDataPublisher(for request: URLRequestConvertible) -> AnyPublisher, Never> { + Empty, Never>().eraseToAnyPublisher() + } + + @available(*, deprecated, message: "Not implemented") + public func uploadMultipartFormData(multipartFormData: @escaping (MultipartFormData) -> Void, + to request: URLRequestConvertible, + completion: @escaping (Data?, Error?) -> Void) { } +} From ae2f7ecccadd74c18ce3a1824f30c72880c3c0b4 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 14 Dec 2022 08:07:54 +0530 Subject: [PATCH 03/15] Add `ApplicationPasswordMapper` to map application password generation response. --- .../Networking.xcodeproj/project.pbxproj | 4 ++++ .../Mapper/ApplicationPasswordMapper.swift | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 Networking/Networking/Mapper/ApplicationPasswordMapper.swift diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index 2989375eba6..b989c1c28e3 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -731,6 +731,7 @@ E1BAB2C72913FB5800C3982B /* WordPressApiError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BAB2C62913FB5800C3982B /* WordPressApiError.swift */; }; EE54C89F2947782E00A9BF61 /* ApplicationPasswordUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE54C89E2947782E00A9BF61 /* ApplicationPasswordUseCase.swift */; }; EE54C8A5294859D200A9BF61 /* ApplicationPasswordNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE54C8A4294859D200A9BF61 /* ApplicationPasswordNetwork.swift */; }; + EE54C8A729486B6800A9BF61 /* ApplicationPasswordMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE54C8A629486B6800A9BF61 /* ApplicationPasswordMapper.swift */; }; EE8A86F1286C5226003E8AA4 /* media-update-product-id-in-wordpress-site.json in Resources */ = {isa = PBXBuildFile; fileRef = EE8A86F0286C5226003E8AA4 /* media-update-product-id-in-wordpress-site.json */; }; EECB7EE8286555180028C888 /* media-update-product-id.json in Resources */ = {isa = PBXBuildFile; fileRef = EECB7EE7286555180028C888 /* media-update-product-id.json */; }; FE28F6E226840DED004465C7 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE28F6E126840DED004465C7 /* User.swift */; }; @@ -1494,6 +1495,7 @@ E1BAB2C62913FB5800C3982B /* WordPressApiError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressApiError.swift; sourceTree = ""; }; EE54C89E2947782E00A9BF61 /* ApplicationPasswordUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordUseCase.swift; sourceTree = ""; }; EE54C8A4294859D200A9BF61 /* ApplicationPasswordNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordNetwork.swift; sourceTree = ""; }; + EE54C8A629486B6800A9BF61 /* ApplicationPasswordMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordMapper.swift; sourceTree = ""; }; EE8A86F0286C5226003E8AA4 /* media-update-product-id-in-wordpress-site.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "media-update-product-id-in-wordpress-site.json"; sourceTree = ""; }; EECB7EE7286555180028C888 /* media-update-product-id.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "media-update-product-id.json"; sourceTree = ""; }; F3F25DC15EC1D7C631169CB5 /* Pods_Networking.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Networking.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2379,6 +2381,7 @@ DE34051828BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift */, 68CB800D28D8901B00E169F8 /* CustomerMapper.swift */, 68F48B0C28E3B2E80045C15B /* WCAnalyticsCustomerMapper.swift */, + EE54C8A629486B6800A9BF61 /* ApplicationPasswordMapper.swift */, ); path = Mapper; sourceTree = ""; @@ -3025,6 +3028,7 @@ FE28F6E6268429B6004465C7 /* UserRemote.swift in Sources */, 7452387421124B7700A973CD /* AnyDecodable.swift in Sources */, CEF88DAD233E95B000BED485 /* OrderRefundCondensed.swift in Sources */, + EE54C8A729486B6800A9BF61 /* ApplicationPasswordMapper.swift in Sources */, FE28F6E426842848004465C7 /* UserMapper.swift in Sources */, CE430670234B99F50073CBFF /* RefundsRemote.swift in Sources */, B56C1EB620EA757B00D749F9 /* SiteListMapper.swift in Sources */, diff --git a/Networking/Networking/Mapper/ApplicationPasswordMapper.swift b/Networking/Networking/Mapper/ApplicationPasswordMapper.swift new file mode 100644 index 00000000000..02d73251907 --- /dev/null +++ b/Networking/Networking/Mapper/ApplicationPasswordMapper.swift @@ -0,0 +1,20 @@ +import Foundation + +struct ApplicationPasswordMapper: Mapper { + private struct ApplicationPassword: Decodable { + let password: String + } + + private struct ApplicationPasswordEnvelope: Decodable { + let password: ApplicationPassword + + private enum CodingKeys: String, CodingKey { + case password = "data" + } + } + + func map(response: Data) throws -> String { + let decoder = JSONDecoder() + return try decoder.decode(ApplicationPasswordEnvelope.self, from: response).password.password + } +} From c330d5fc052a804111f7d7d58ca3299f35a1fc57 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 14 Dec 2022 08:17:49 +0530 Subject: [PATCH 04/15] Introduce `DefaultApplicationPasswordUseCase` to perform application password actions. --- .../ApplicationPasswordUseCase.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift index 908146c5ed2..69e28bab85b 100644 --- a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift +++ b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift @@ -1,5 +1,6 @@ import Foundation import WordPressShared +import KeychainAccess struct ApplicationPassword { /// WordPress org username that the application password belongs to @@ -28,3 +29,44 @@ protocol ApplicationPasswordUseCase { /// func deletePassword() async throws } + +final class DefaultApplicationPasswordUseCase { + /// WordPress.com Credentials. + /// + private let credentials: Credentials + + /// SiteID needed when using WPCOM credentials + /// + private let siteID: Int64 + + /// To generate and delete application password + /// + private let network: Network + + /// Stores the application password + /// + private let keychain: Keychain + + /// Used to name the password in wpadmin. + /// + private var applicationPasswordName: String { + get async { + await UIDevice.current.model + } + } + + init(siteID: Int64, + networkcredentials: Credentials, + network: Network? = nil, + keychain: Keychain = Keychain(service: "com.automattic.woocommerce.applicationpassword")) { + self.siteID = siteID + self.credentials = networkcredentials + self.keychain = keychain + + if let network { + self.network = network + } else { + self.network = ApplicationPasswordNetwork(credentials: networkcredentials) + } + } +} From 325e1d0f4b4f75c799dfde76349031b4038b36d9 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 14 Dec 2022 08:23:28 +0530 Subject: [PATCH 05/15] Add ability to store password to keychain. --- .../ApplicationPasswordUseCase.swift | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift index 69e28bab85b..06414d9c00a 100644 --- a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift +++ b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift @@ -69,4 +69,42 @@ final class DefaultApplicationPasswordUseCase { self.network = ApplicationPasswordNetwork(credentials: networkcredentials) } } + + /// Returns the locally saved ApplicationPassword if available + /// + var applicationPassword: ApplicationPassword? { + guard let password = keychain.applicationPassword, + let username = keychain.applicationPasswordUsername else { + return nil + } + return ApplicationPassword(wpOrgUsername: username, password: Secret(password)) + } +} + +private extension DefaultApplicationPasswordUseCase { + /// Saves application password into keychain + /// + /// - Parameter password: `ApplicationPasword` to be saved + /// + func saveApplicationPassword(_ password: ApplicationPassword) { + keychain.applicationPassword = password.wpOrgUsername + keychain.applicationPasswordUsername = password.password.secretValue + } +} + +// MARK: - For storing the application password in keychain +// +private extension Keychain { + private static let keychainApplicationPassword = "ApplicationPassword" + private static let keychainApplicationPasswordUsername = "ApplicationPasswordUsername" + + var applicationPassword: String? { + get { self[Keychain.keychainApplicationPassword] } + set { self[Keychain.keychainApplicationPassword] = newValue } + } + + var applicationPasswordUsername: String? { + get { self[Keychain.keychainApplicationPasswordUsername] } + set { self[Keychain.keychainApplicationPasswordUsername] = newValue } + } } From cacc16ea3d7d7e442fb8f42ab0ce98bf1e535ac4 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 14 Dec 2022 08:33:29 +0530 Subject: [PATCH 06/15] Generate application password and handle errors. --- .../ApplicationPasswordUseCase.swift | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift index 06414d9c00a..71f1f40f412 100644 --- a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift +++ b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift @@ -2,6 +2,11 @@ import Foundation import WordPressShared import KeychainAccess +enum ApplicationPasswordUseCaseError: Error { + case duplicateName + case applicationPasswordsDisabled +} + struct ApplicationPassword { /// WordPress org username that the application password belongs to /// @@ -82,6 +87,40 @@ final class DefaultApplicationPasswordUseCase { } private extension DefaultApplicationPasswordUseCase { + /// Creates application password using WordPress.com authentication token + /// + /// - Returns: Application password as `String` + /// + func createApplicationPasswordUsingWPCOMAuthToken() async throws -> String { + let passwordName = await applicationPasswordName + + let parameters = [ParameterKey.name: passwordName] + let request = JetpackRequest(wooApiVersion: .none, method: .post, siteID: siteID, path: Path.applicationPasswords, parameters: parameters) + + return try await withCheckedThrowingContinuation { continuation in + network.responseData(for: request) { result in + switch result { + case .success(let data): + do { + let validator = request.responseDataValidator() + try validator.validate(data: data) + let mapper = ApplicationPasswordMapper() + let password = try mapper.map(response: data) + continuation.resume(returning: password) + } catch let DotcomError.unknown(code, _) where code == ErrorCode.applicationPasswordsDisabledErrorCode { + continuation.resume(throwing: ApplicationPasswordUseCaseError.applicationPasswordsDisabled) + } catch let DotcomError.unknown(code, _) where code == ErrorCode.duplicateNameErrorCode { + continuation.resume(throwing: ApplicationPasswordUseCaseError.duplicateName) + } catch { + continuation.resume(throwing: error) + } + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + /// Saves application password into keychain /// /// - Parameter password: `ApplicationPasword` to be saved @@ -92,6 +131,23 @@ private extension DefaultApplicationPasswordUseCase { } } +// MARK: - Constants +// +private extension DefaultApplicationPasswordUseCase { + enum Path { + static let applicationPasswords = "wp/v2/users/me/application-passwords" + } + + enum ParameterKey { + static let name = "name" + } + + enum ErrorCode { + static let applicationPasswordsDisabledErrorCode = "application_passwords_disabled" + static let duplicateNameErrorCode = "application_password_duplicate_name" + } +} + // MARK: - For storing the application password in keychain // private extension Keychain { From de36ede1bf7e9db03ee6e30da491e241badfb1f2 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 14 Dec 2022 08:42:28 +0530 Subject: [PATCH 07/15] Fetch wpadmin username using WPCOM token. --- .../ApplicationPasswordUseCase.swift | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift index 71f1f40f412..74c64d67c8f 100644 --- a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift +++ b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift @@ -121,6 +121,39 @@ private extension DefaultApplicationPasswordUseCase { } } + /// Fetches wpadmin username using WordPress.com authentication token + /// + /// - Returns: wpadmin username + /// + func fetchWPAdminUsername() async throws -> String { + let parameters = [ + "context": "edit", + "fields": "id,username,id_wpcom,email,first_name,last_name,nickname,roles" + ] + let request = JetpackRequest(wooApiVersion: .none, method: .get, siteID: siteID, path: Path.users, parameters: parameters) + + return try await withCheckedThrowingContinuation { continuation in + network.responseData(for: request) { [weak self] result in + guard let self else { return } + + switch result { + case .success(let data): + do { + let validator = request.responseDataValidator() + try validator.validate(data: data) + let mapper = UserMapper(siteID: self.siteID) + let username = try mapper.map(response: data).username + continuation.resume(returning: username) + } catch { + continuation.resume(throwing: error) + } + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + /// Saves application password into keychain /// /// - Parameter password: `ApplicationPasword` to be saved @@ -136,6 +169,7 @@ private extension DefaultApplicationPasswordUseCase { private extension DefaultApplicationPasswordUseCase { enum Path { static let applicationPasswords = "wp/v2/users/me/application-passwords" + static let users = "wp/v2/users/me" } enum ParameterKey { From a0b8e16b5efbf216c1ed07caa4544f130fcef3e3 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 14 Dec 2022 08:45:13 +0530 Subject: [PATCH 08/15] Delete application password using WPCOM authentication token. --- .../ApplicationPasswordUseCase.swift | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift index 74c64d67c8f..de9d9a01fc0 100644 --- a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift +++ b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift @@ -154,6 +154,37 @@ private extension DefaultApplicationPasswordUseCase { } } + /// Deletes application password using WordPress.com authentication token + /// + func deleteApplicationPasswordUsingWPCOMAuthToken() async throws { + + // Delete password from keychain + keychain.applicationPassword = nil + keychain.applicationPasswordUsername = nil + + let passwordName = await applicationPasswordName + + let parameters = [ParameterKey.name: passwordName] + let request = JetpackRequest(wooApiVersion: .none, method: .delete, siteID: siteID, path: Path.applicationPasswords, parameters: parameters) + + try await withCheckedThrowingContinuation { continuation in + network.responseData(for: request) { result in + switch result { + case .success(let data): + do { + let validator = request.responseDataValidator() + try validator.validate(data: data) + continuation.resume() + } catch { + continuation.resume(throwing: error) + } + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + /// Saves application password into keychain /// /// - Parameter password: `ApplicationPasword` to be saved From cdc20de683d7fda9601da59238230cfc397afe96 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 14 Dec 2022 08:53:42 +0530 Subject: [PATCH 09/15] Conform to `ApplicationPasswordUseCase` --- .../ApplicationPasswordUseCase.swift | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift index de9d9a01fc0..382988b1e9e 100644 --- a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift +++ b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift @@ -35,7 +35,7 @@ protocol ApplicationPasswordUseCase { func deletePassword() async throws } -final class DefaultApplicationPasswordUseCase { +final class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase { /// WordPress.com Credentials. /// private let credentials: Credentials @@ -84,6 +84,27 @@ final class DefaultApplicationPasswordUseCase { } return ApplicationPassword(wpOrgUsername: username, password: Secret(password)) } + + /// Generates new ApplicationPassword + /// + /// - Returns: Generated `ApplicationPassword` instance + /// + func generateNewPassword() async throws -> ApplicationPassword { + let password = try await createApplicationPasswordUsingWPCOMAuthToken() + let username = try await fetchWPAdminUsername() + + let applicationPassword = ApplicationPassword(wpOrgUsername: username, password: Secret(password)) + saveApplicationPassword(applicationPassword) + return applicationPassword + } + + /// Deletes the application password + /// + /// Deletes locally and also sends an API request to delete it from the site + /// + func deletePassword() async throws { + try await deleteApplicationPasswordUsingWPCOMAuthToken() + } } private extension DefaultApplicationPasswordUseCase { From 6b63664ddea50ffe10b0e98463bba6ea3a67aa3e Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 14 Dec 2022 12:25:02 +0530 Subject: [PATCH 10/15] Change keychain service name to match WooCommerce. --- .../ApplicationPassword/ApplicationPasswordUseCase.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift index 382988b1e9e..a66aa650b48 100644 --- a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift +++ b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift @@ -63,7 +63,7 @@ final class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase { init(siteID: Int64, networkcredentials: Credentials, network: Network? = nil, - keychain: Keychain = Keychain(service: "com.automattic.woocommerce.applicationpassword")) { + keychain: Keychain = Keychain(service: KeychainServiceName.name)) { self.siteID = siteID self.credentials = networkcredentials self.keychain = keychain @@ -219,6 +219,12 @@ private extension DefaultApplicationPasswordUseCase { // MARK: - Constants // private extension DefaultApplicationPasswordUseCase { + enum KeychainServiceName { + /// Matching `WooConstants.keychainServiceName` + /// + static let name = "com.automattic.woocommerce" + } + enum Path { static let applicationPasswords = "wp/v2/users/me/application-passwords" static let users = "wp/v2/users/me" From d905e7a1d199b83780b9b9c22b46add3d5888b56 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 14 Dec 2022 12:26:05 +0530 Subject: [PATCH 11/15] Change application password name format to include bundle identifier. --- .../ApplicationPassword/ApplicationPasswordUseCase.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift index a66aa650b48..027f2d2e238 100644 --- a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift +++ b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift @@ -56,7 +56,9 @@ final class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase { /// private var applicationPasswordName: String { get async { - await UIDevice.current.model + let bundleIdentifier = Bundle.main.bundleIdentifier ?? "Unknown" + let model = await UIDevice.current.model + return bundleIdentifier + ".ios-app-client." + model } } From 38c7deca8e118b8ceff9599a758fad33ecfabf1f Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 14 Dec 2022 13:19:08 +0530 Subject: [PATCH 12/15] Delete password and try again when `duplicatName` error occurs. --- .../ApplicationPasswordUseCase.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift index 027f2d2e238..ae6ca266aaa 100644 --- a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift +++ b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift @@ -89,10 +89,19 @@ final class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase { /// Generates new ApplicationPassword /// + /// When `duplicateName` error occurs this method will delete the password and try generating again + /// /// - Returns: Generated `ApplicationPassword` instance /// func generateNewPassword() async throws -> ApplicationPassword { - let password = try await createApplicationPasswordUsingWPCOMAuthToken() + let password = try await { + do { + return try await createApplicationPasswordUsingWPCOMAuthToken() + } catch ApplicationPasswordUseCaseError.duplicateName { + try await deletePassword() + return try await createApplicationPasswordUsingWPCOMAuthToken() + } + }() let username = try await fetchWPAdminUsername() let applicationPassword = ApplicationPassword(wpOrgUsername: username, password: Secret(password)) @@ -180,7 +189,6 @@ private extension DefaultApplicationPasswordUseCase { /// Deletes application password using WordPress.com authentication token /// func deleteApplicationPasswordUsingWPCOMAuthToken() async throws { - // Delete password from keychain keychain.applicationPassword = nil keychain.applicationPasswordUsername = nil From b2f3d741f6009f82b03a4e7fcc0ab51da3ca977d Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 14 Dec 2022 13:24:07 +0530 Subject: [PATCH 13/15] Explain why we need `ApplicationPasswordNetwork`. --- .../Networking/Network/ApplicationPasswordNetwork.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Networking/Networking/Network/ApplicationPasswordNetwork.swift b/Networking/Networking/Network/ApplicationPasswordNetwork.swift index cbebc9beeba..62c10ec6401 100644 --- a/Networking/Networking/Network/ApplicationPasswordNetwork.swift +++ b/Networking/Networking/Network/ApplicationPasswordNetwork.swift @@ -2,6 +2,11 @@ import Combine import Foundation import Alamofire +/// This Network is specific for generating and deleting application passwords +/// +/// - We cannot use the AlamofireNetwork as we will be initiating the application password generation from there. (By listening to other API calls) +/// - `ApplicationPasswordNetwork` currently takes in WPCOM credentials. In future it will also work with .org site credentials as well. +/// public class ApplicationPasswordNetwork: Network { /// WordPress.com Credentials. /// From 76a8f12d3a22f30cccac6727ce73e67948b33c5c Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 14 Dec 2022 17:19:49 +0530 Subject: [PATCH 14/15] Send password generation and username fetching in parallel. --- .../ApplicationPassword/ApplicationPasswordUseCase.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift index ae6ca266aaa..211a0dac103 100644 --- a/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift +++ b/Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift @@ -94,7 +94,7 @@ final class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase { /// - Returns: Generated `ApplicationPassword` instance /// func generateNewPassword() async throws -> ApplicationPassword { - let password = try await { + async let password = try { do { return try await createApplicationPasswordUsingWPCOMAuthToken() } catch ApplicationPasswordUseCaseError.duplicateName { @@ -102,9 +102,9 @@ final class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase { return try await createApplicationPasswordUsingWPCOMAuthToken() } }() - let username = try await fetchWPAdminUsername() + async let username = try fetchWPAdminUsername() - let applicationPassword = ApplicationPassword(wpOrgUsername: username, password: Secret(password)) + let applicationPassword = try await ApplicationPassword(wpOrgUsername: username, password: Secret(password)) saveApplicationPassword(applicationPassword) return applicationPassword } From 7b5dbe04c946e55afc94352b6e30f561c1f6b3fa Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Thu, 15 Dec 2022 10:56:15 +0100 Subject: [PATCH 15/15] Add missing `networking_pods` dependencies to Yosemite in `Podfile` --- Podfile | 34 ++++++++++++++++------------------ Podfile.lock | 2 +- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/Podfile b/Podfile index c3770c3c794..1db37333421 100644 --- a/Podfile +++ b/Podfile @@ -56,6 +56,21 @@ def stripe_terminal pod 'StripeTerminal', '~> 2.14' end +def networking_pods + alamofire + cocoa_lumberjack + + pod 'Sourcery', '~> 1.0.3', configuration: 'Debug' + + # Used for HTML parsing + aztec + + # Used for storing application password + keychain + + wordpress_kit +end + # Main Target! # ============ # @@ -122,6 +137,7 @@ def yosemite_pods stripe_terminal cocoa_lumberjack wordpress_kit + networking_pods aztec end @@ -165,24 +181,6 @@ target 'WooFoundationTests' do woofoundation_pods end -# Networking Layer: -# ================= -# -def networking_pods - alamofire - cocoa_lumberjack - - pod 'Sourcery', '~> 1.0.3', configuration: 'Debug' - - # Used for HTML parsing - aztec - - # Used for storing application password - keychain - - wordpress_kit -end - # Networking Target: # ================== # diff --git a/Podfile.lock b/Podfile.lock index 7153ab271aa..9f11c86db3f 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -169,6 +169,6 @@ SPEC CHECKSUMS: ZendeskSupportProvidersSDK: 2bdf8544f7cd0fd4c002546f5704b813845beb2a ZendeskSupportSDK: 3a8e508ab1d9dd22dc038df6c694466414e037ba -PODFILE CHECKSUM: 04b17163281ec1c5967ef28a56f130d10c73bce9 +PODFILE CHECKSUM: 670bb1b07306bd212f79b8afb1ebed4fc6eaa7b7 COCOAPODS: 1.11.3