From 33f52950ddf5ab24851ad859920d96c4cacd0ad8 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 13 May 2026 23:00:39 +1200 Subject: [PATCH 1/2] Format site enum files --- .../WordPressData/Swift/Blog+Features.swift | 10 ++- .../WordPressData/Swift/Blog+SelfHosted.swift | 59 +++++++++++---- .../ApplicationPasswordsRepositoryTests.swift | 42 +++++++---- .../ApplicationPasswordRequiredView.swift | 35 +++++++-- .../Editor/EditorDependencyManager.swift | 10 ++- .../CustomPostTypeService.swift | 5 +- .../Login/JetpackConnectionViewModel.swift | 72 +++++++++++++------ 7 files changed, 168 insertions(+), 65 deletions(-) diff --git a/Sources/WordPressData/Swift/Blog+Features.swift b/Sources/WordPressData/Swift/Blog+Features.swift index c95f482adf89..577bc896dfd2 100644 --- a/Sources/WordPressData/Swift/Blog+Features.swift +++ b/Sources/WordPressData/Swift/Blog+Features.swift @@ -82,7 +82,9 @@ extension Blog { case .customThemes: return supportsRestAPI && isAdmin && !isHostedAtWPcom case .premiumThemes: - return supports(.customThemes) && (planID?.intValue == Self.jetpackProfessionalYearlyPlanId || planID?.intValue == Self.jetpackProfessionalMonthlyPlanId) + return supports(.customThemes) + && (planID?.intValue == Self.jetpackProfessionalYearlyPlanId + || planID?.intValue == Self.jetpackProfessionalMonthlyPlanId) case .private: return isHostedAtWPcom case .sharing: @@ -192,7 +194,8 @@ private extension Blog { return true } if account == nil && !isHostedAtWPcom && selfHostedSiteRestApi != nil - && hasRequiredWordPressVersion("5.5") { + && hasRequiredWordPressVersion("5.5") + { return true } return false @@ -207,7 +210,8 @@ private extension Blog { func hasRequiredJetpackVersion(_ requiredVersion: String) -> Bool { guard supportsRestAPI, !isHostedAtWPcom, - let version = jetpack?.version else { + let version = jetpack?.version + else { return false } return version.compare(requiredVersion, options: .numeric) != .orderedAscending diff --git a/Sources/WordPressData/Swift/Blog+SelfHosted.swift b/Sources/WordPressData/Swift/Blog+SelfHosted.swift index f7d8b63f86b9..bd59bd9aa6d2 100644 --- a/Sources/WordPressData/Swift/Blog+SelfHosted.swift +++ b/Sources/WordPressData/Swift/Blog+SelfHosted.swift @@ -26,12 +26,13 @@ public extension Blog { using keychainImplementation: KeychainAccessible = KeychainUtils() ) async throws -> TaggedManagedObjectID { try await contextManager.performAndSave { context in - let blog = if let blogID { - try context.existingObject(with: blogID) - } else { - Blog.lookup(username: details.userLogin, xmlrpc: xmlrpcEndpointURL.absoluteString, in: context) - ?? Blog.createBlankBlog(in: context) - } + let blog = + if let blogID { + try context.existingObject(with: blogID) + } else { + Blog.lookup(username: details.userLogin, xmlrpc: xmlrpcEndpointURL.absoluteString, in: context) + ?? Blog.createBlankBlog(in: context) + } blog.url = details.siteUrl blog.username = details.userLogin @@ -104,12 +105,19 @@ public extension Blog { /// A null-safe replacement for `Blog.password(get)` func getPassword(using keychainImplementation: KeychainAccessible = KeychainUtils()) throws -> String { - try keychainImplementation.getPassword(for: self.getUsername(), serviceName: self.getXMLRPCEndpoint().absoluteString) + try keychainImplementation.getPassword( + for: self.getUsername(), + serviceName: self.getXMLRPCEndpoint().absoluteString + ) } /// A null-safe replacement for `Blog.password(set)` func setPassword(to newValue: String, using keychainImplementation: KeychainAccessible = KeychainUtils()) throws { - try keychainImplementation.setPassword(for: self.getUsername(), to: newValue, serviceName: self.getXMLRPCEndpoint().absoluteString) + try keychainImplementation.setPassword( + for: self.getUsername(), + to: newValue, + serviceName: self.getXMLRPCEndpoint().absoluteString + ) } func wordPressClientParsedUrl() throws -> ParsedUrl { @@ -192,16 +200,29 @@ public extension WpApiApplicationPasswordDetails { public enum WordPressSite: Hashable { case dotCom(siteURL: URL, siteId: Int, authToken: String) - case selfHosted(blogId: TaggedManagedObjectID, siteURL: URL, apiRootURL: ParsedUrl, username: String, authToken: String) + case selfHosted( + blogId: TaggedManagedObjectID, + siteURL: URL, + apiRootURL: ParsedUrl, + username: String, + authToken: String + ) public init(blog: Blog) throws { let siteURL = try blog.getUrl() // Directly access the site content when available. if let restApiRootURL = blog.restApiRootURL, - let restApiRootURL = try? ParsedUrl.parse(input: restApiRootURL), - let username = blog.username, - let authToken = try? blog.getApplicationToken() { - self = .selfHosted(blogId: TaggedManagedObjectID(blog), siteURL: siteURL, apiRootURL: restApiRootURL, username: username, authToken: authToken) + let restApiRootURL = try? ParsedUrl.parse(input: restApiRootURL), + let username = blog.username, + let authToken = try? blog.getApplicationToken() + { + self = .selfHosted( + blogId: TaggedManagedObjectID(blog), + siteURL: siteURL, + apiRootURL: restApiRootURL, + username: username, + authToken: authToken + ) } else if let account = blog.account, let siteId = blog.dotComID?.intValue { // When the site is added via a WP.com account, access the site via WP.com let authToken = try account.authToken ?? WPAccount.token(forUsername: account.username) @@ -210,8 +231,16 @@ public enum WordPressSite: Hashable { // In theory, this branch should never run, because the two if statements above should have covered all paths. // But we'll keep it here as the fallback. let url = try blog.getUrl() - let apiRootURL = try ParsedUrl.parse(input: blog.restApiRootURL ?? blog.getUrl().appending(path: "wp-json").absoluteString) - self = .selfHosted(blogId: TaggedManagedObjectID(blog), siteURL: url, apiRootURL: apiRootURL, username: try blog.getUsername(), authToken: try blog.getApplicationToken()) + let apiRootURL = try ParsedUrl.parse( + input: blog.restApiRootURL ?? blog.getUrl().appending(path: "wp-json").absoluteString + ) + self = .selfHosted( + blogId: TaggedManagedObjectID(blog), + siteURL: url, + apiRootURL: apiRootURL, + username: try blog.getUsername(), + authToken: try blog.getApplicationToken() + ) } } diff --git a/Tests/KeystoneTests/Tests/Utility/ApplicationPasswordsRepositoryTests.swift b/Tests/KeystoneTests/Tests/Utility/ApplicationPasswordsRepositoryTests.swift index 69603e8cf564..8f962d6c059f 100644 --- a/Tests/KeystoneTests/Tests/Utility/ApplicationPasswordsRepositoryTests.swift +++ b/Tests/KeystoneTests/Tests/Utility/ApplicationPasswordsRepositoryTests.swift @@ -21,20 +21,24 @@ class ApplicationPasswordsRepositoryTests { @Test func simpleSite() async throws { - defer { HTTPStubs.removeAllStubs()} + defer { HTTPStubs.removeAllStubs() } try await signInWPComAccount() let blog = try await createSimpleSite() let repository = ApplicationPasswordRepository.forTesting(coreDataStack: coreDataStack, keychain: keychain) - await #expect(throws: AutoDiscoveryAttemptFailure.self, "Simple site does not support application passwords", performing: { - try await repository.createPasswordIfNeeded(for: blog) - }) + await #expect( + throws: AutoDiscoveryAttemptFailure.self, + "Simple site does not support application passwords", + performing: { + try await repository.createPasswordIfNeeded(for: blog) + } + ) } @Test func atomicSite() async throws { - defer { HTTPStubs.removeAllStubs()} + defer { HTTPStubs.removeAllStubs() } try await signInWPComAccount() let blog = try await createAtomicSite() @@ -52,7 +56,7 @@ class ApplicationPasswordsRepositoryTests { @Test func selfHostedSite() async throws { - defer { HTTPStubs.removeAllStubs()} + defer { HTTPStubs.removeAllStubs() } let uuid = UUID().uuidString.lowercased() let host = "\(uuid).example.com" @@ -71,7 +75,7 @@ class ApplicationPasswordsRepositoryTests { @Test func selfHostedSiteWithInaccessibleRestApi() async throws { - defer { HTTPStubs.removeAllStubs()} + defer { HTTPStubs.removeAllStubs() } let host = "2.example.com" let blog = try await createSelfHostedSite(host: host) @@ -81,14 +85,18 @@ class ApplicationPasswordsRepositoryTests { stub(condition: isHost(host) && isPath("/wp-login.php")) { _ in HTTPStubsResponse(data: "Logged in".data(using: .utf8)!, statusCode: 200, headers: nil) } - stub(condition: isHost(host) && isPath("/wp-admin/admin-ajax.php") && containsQueryParams(["action": "rest-nonce"])) { _ in + stub( + condition: isHost(host) && isPath("/wp-admin/admin-ajax.php") + && containsQueryParams(["action": "rest-nonce"]) + ) { _ in HTTPStubsResponse(data: "not allowed".data(using: .utf8)!, statusCode: 400, headers: nil) } stub(condition: isHost(host) && isPath("/wp-admin/post-new.php")) { _ in HTTPStubsResponse(data: "not allowed".data(using: .utf8)!, statusCode: 400, headers: nil) } stub(condition: isHost(host) && isPath("/wp-json/wp/v2/users/me")) { _ in - let json = #"{"code":"rest_not_logged_in","message":"You are not currently logged in.","data":{"status":401}}"# + let json = + #"{"code":"rest_not_logged_in","message":"You are not currently logged in.","data":{"status":401}}"# return HTTPStubsResponse(data: json.data(using: .utf8)!, statusCode: 401, headers: nil) } @@ -101,7 +109,7 @@ class ApplicationPasswordsRepositoryTests { @Test func concurrentCalls() async throws { - defer { HTTPStubs.removeAllStubs()} + defer { HTTPStubs.removeAllStubs() } let host = "3.example.com" let blog = try await createSelfHostedSite(host: host) @@ -135,7 +143,7 @@ class ApplicationPasswordsRepositoryTests { @Test func cancel() async throws { - defer { HTTPStubs.removeAllStubs()} + defer { HTTPStubs.removeAllStubs() } let uuid = UUID().uuidString.lowercased() let host = "\(uuid).example.com" @@ -163,7 +171,7 @@ class ApplicationPasswordsRepositoryTests { @Test func cancelFirstCall() async throws { - defer { HTTPStubs.removeAllStubs()} + defer { HTTPStubs.removeAllStubs() } let uuid = UUID().uuidString.lowercased() let host = "\(uuid).example.com" @@ -197,7 +205,7 @@ class ApplicationPasswordsRepositoryTests { @Test(arguments: [1, 2, 3, 4]) func cancelConcurrentCall(nthTaskToBeCancelled: Int) async throws { - defer { HTTPStubs.removeAllStubs()} + defer { HTTPStubs.removeAllStubs() } let uuid = UUID().uuidString.lowercased() let host = "\(uuid).example.com" @@ -307,7 +315,10 @@ private extension ApplicationPasswordsRepositoryTests { stub(condition: isHost(host) && isPath("/wp-login.php")) { _ in HTTPStubsResponse(data: "Logged in".data(using: .utf8)!, statusCode: 200, headers: nil) } - stub(condition: isHost(host) && isPath("/wp-admin/admin-ajax.php") && containsQueryParams(["action": "rest-nonce"])) { _ in + stub( + condition: isHost(host) && isPath("/wp-admin/admin-ajax.php") + && containsQueryParams(["action": "rest-nonce"]) + ) { _ in HTTPStubsResponse(data: "abcd".data(using: .utf8)!, statusCode: 200, headers: nil) } stub(condition: isHost(host) && isPath("/wp-json/wp/v2/users/me/application-passwords")) { _ in @@ -343,7 +354,8 @@ private extension ApplicationPasswordsRepositoryTests { } func stubJetpackProxyCreateApplicationPassword(siteId: Int, password: String) { - stub(condition: isHost("public-api.wordpress.com") && isPath("/rest/v1.1/jetpack-blogs/\(siteId)/rest-api")) { _ in + stub(condition: isHost("public-api.wordpress.com") && isPath("/rest/v1.1/jetpack-blogs/\(siteId)/rest-api")) { + _ in let json = """ { "data": { diff --git a/WordPress/Classes/Login/ApplicationPasswordRequiredView.swift b/WordPress/Classes/Login/ApplicationPasswordRequiredView.swift index d65269e314b4..213a58c01c14 100644 --- a/WordPress/Classes/Login/ApplicationPasswordRequiredView.swift +++ b/WordPress/Classes/Login/ApplicationPasswordRequiredView.swift @@ -16,7 +16,13 @@ struct ApplicationPasswordRequiredView: View { weak var presentingViewController: UIViewController? - init(blog: Blog, localizedFeatureName: String, source: String, presentingViewController: UIViewController, @ViewBuilder content: @escaping (WordPressClient) -> Content) { + init( + blog: Blog, + localizedFeatureName: String, + source: String, + presentingViewController: UIViewController, + @ViewBuilder content: @escaping (WordPressClient) -> Content + ) { self.blog = blog self.localizedFeatureName = localizedFeatureName self.source = source @@ -101,7 +107,11 @@ struct ApplicationPasswordRequiredView: View { do { // Get an application password for the given site. let authenticator = SelfHostedSiteAuthenticator() - let _ = try await authenticator.signIn(site: url, from: presenter, context: .reauthentication(TaggedManagedObjectID(blog), username: blog.username)) + let _ = try await authenticator.signIn( + site: url, + from: presenter, + context: .reauthentication(TaggedManagedObjectID(blog), username: blog.username) + ) WPAnalytics.track( .applicationPasswordCreated, @@ -135,14 +145,29 @@ struct ApplicationPasswordRequiredView: View { enum Strings { static var siteUrlNotFoundError: String { - NSLocalizedString("applicationPasswordMigration.error.siteUrlNotFound", value: "Cannot find the current site's url", comment: "Error message when the current site's url cannot be found") + NSLocalizedString( + "applicationPasswordMigration.error.siteUrlNotFound", + value: "Cannot find the current site's url", + comment: "Error message when the current site's url cannot be found" + ) } static func userNameMismatch(expected: String) -> String { - let format = NSLocalizedString("applicationPasswordMigration.error.usernameMismatch", value: "You need to sign in with user \"%@\"", comment: "Error message when the username does not match the signed-in user. The first argument is the currently signed in user's user login name") + let format = NSLocalizedString( + "applicationPasswordMigration.error.usernameMismatch", + value: "You need to sign in with user \"%@\"", + comment: + "Error message when the username does not match the signed-in user. The first argument is the currently signed in user's user login name" + ) return String(format: format, expected) } - static var unsupported: String { NSLocalizedString("applicationPasswordMigration.error.unsupported", value: "This site does not support Application Passwords.", comment: "Error message shown when the site doesn't support Application Passwords feature") } + static var unsupported: String { + NSLocalizedString( + "applicationPasswordMigration.error.unsupported", + value: "This site does not support Application Passwords.", + comment: "Error message shown when the site doesn't support Application Passwords feature" + ) + } } } diff --git a/WordPress/Classes/Utility/Editor/EditorDependencyManager.swift b/WordPress/Classes/Utility/Editor/EditorDependencyManager.swift index 4c4c51e4844d..290c7daba28f 100644 --- a/WordPress/Classes/Utility/Editor/EditorDependencyManager.swift +++ b/WordPress/Classes/Utility/Editor/EditorDependencyManager.swift @@ -55,7 +55,8 @@ final class EditorDependencyManager: Sendable { $0.featureFlagObserver = NotificationCenter.default .publisher(for: FeatureFlagOverrideStore.didChangeNotification) .filter { - ($0.userInfo?[FeatureFlagOverrideStore.notificationFeatureFlagKey] as? RemoteFeatureFlag) == .newGutenberg + ($0.userInfo?[FeatureFlagOverrideStore.notificationFeatureFlagKey] as? RemoteFeatureFlag) + == .newGutenberg } .sink { [weak self] _ in Task { @@ -203,7 +204,8 @@ final class EditorDependencyManager: Sendable { // dependencies cache (the "slow path") creates on-disk caches that // EditorDependencyManager doesn't track. We should consider exposing // GutenbergKit's cache to access and/or track these slow-path caches. - let postTypes = keysToInvalidate.isEmpty + let postTypes = + keysToInvalidate.isEmpty ? [PostTypeDetails.post] : keysToInvalidate.map(\.postType) @@ -222,7 +224,9 @@ final class EditorDependencyManager: Sendable { do { try await EditorService(configuration: configuration).purge() } catch { - DDLogError("EditorDependencyManager: Failed to clear cache for \(configuration.postType.postType): \(error)") + DDLogError( + "EditorDependencyManager: Failed to clear cache for \(configuration.postType.postType): \(error)" + ) } } } diff --git a/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostTypeService.swift b/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostTypeService.swift index 51faf14c639d..43f583787502 100644 --- a/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostTypeService.swift +++ b/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostTypeService.swift @@ -18,8 +18,9 @@ class CustomPostTypeService { init?(blog: Blog) { guard FeatureFlag.customPostTypes.enabled, - let site = try? WordPressSite(blog: blog), - case .selfHosted = site else { return nil } + let site = try? WordPressSite(blog: blog), + case .selfHosted = site + else { return nil } self.blog = TaggedManagedObjectID(blog) self.client = WordPressClientFactory.shared.instance(for: site) } diff --git a/WordPress/Classes/ViewRelated/Jetpack/Login/JetpackConnectionViewModel.swift b/WordPress/Classes/ViewRelated/Jetpack/Login/JetpackConnectionViewModel.swift index 55f525cd02f7..5add606f7543 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Login/JetpackConnectionViewModel.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Login/JetpackConnectionViewModel.swift @@ -19,7 +19,12 @@ class JetpackConnectionViewModel: ObservableObject { private let connectionService: JetpackConnectionService private var stepContext: StepContext = .initial - init(blog: Blog, presentingViewController: UIViewController, connectionService: JetpackConnectionService, completionHandler: @escaping () -> Void) { + init( + blog: Blog, + presentingViewController: UIViewController, + connectionService: JetpackConnectionService, + completionHandler: @escaping () -> Void + ) { self.blogID = TaggedManagedObjectID(blog) self.presentingViewController = presentingViewController self.connectionService = connectionService @@ -43,9 +48,12 @@ class JetpackConnectionViewModel: ObservableObject { private func processCurrentStep() async { stepStages[currentStep] = .processing - WPAnalytics.track(currentStep.event, properties: [ - "state": "started" - ]) + WPAnalytics.track( + currentStep.event, + properties: [ + "state": "started" + ] + ) do { switch currentStep { @@ -63,9 +71,12 @@ class JetpackConnectionViewModel: ObservableObject { stepStages[currentStep] = .success - WPAnalytics.track(currentStep.event, properties: [ - "state": "completed" - ]) + WPAnalytics.track( + currentStep.event, + properties: [ + "state": "completed" + ] + ) if let nextStep = self.nextStep() { currentStep = nextStep @@ -77,11 +88,14 @@ class JetpackConnectionViewModel: ObservableObject { } catch { stepStages[currentStep] = .error(error.localizedDescription) - WPAnalytics.track(currentStep.event, properties: [ - "state": "failed", - "error_domain": (error as NSError).domain, - "error_code": (error as NSError).code - ]) + WPAnalytics.track( + currentStep.event, + properties: [ + "state": "failed", + "error_domain": (error as NSError).domain, + "error_code": (error as NSError).code + ] + ) } } @@ -144,9 +158,12 @@ class JetpackConnectionViewModel: ObservableObject { } func retryCurrentStep() { - WPAnalytics.track(.jetpackConnectStepRetried, properties: [ - "step": currentStep.event.value - ]) + WPAnalytics.track( + .jetpackConnectStepRetried, + properties: [ + "step": currentStep.event.value + ] + ) stepStages[currentStep] = .pending Task { await processCurrentStep() @@ -182,13 +199,14 @@ class JetpackConnectionService { guard blog.account == nil else { return nil } if let jetpack = blog.jetpack, jetpack.isInstalled, let version = jetpack.version, - // The `version` value is not a strict semantic version number. - version.compare("14.2", options: .numeric) == .orderedAscending { + // The `version` value is not a strict semantic version number. + version.compare("14.2", options: .numeric) == .orderedAscending + { return nil } guard let site = try? WordPressSite(blog: blog), - case let .selfHosted(_, _, apiRootURL, username, password) = site + case let .selfHosted(_, _, apiRootURL, username, password) = site else { return nil } @@ -202,8 +220,12 @@ class JetpackConnectionService { ) } - func performLogin(from presentingViewController: UIViewController, blogID: TaggedManagedObjectID) async throws -> TaggedManagedObjectID { - let defaultAccount: TaggedManagedObjectID? = try await ContextManager.shared.performQuery { context in + func performLogin( + from presentingViewController: UIViewController, + blogID: TaggedManagedObjectID + ) async throws -> TaggedManagedObjectID { + let defaultAccount: TaggedManagedObjectID? = try await ContextManager.shared.performQuery { + context in guard let account = try WPAccount.lookupDefaultWordPressComAccount(in: context) else { return nil } return .init(account) } @@ -217,7 +239,10 @@ class JetpackConnectionService { } let authenticator = WordPressDotComAuthenticator(showProgressHUD: false) - return try await authenticator.attemptSignIn(from: presentingViewController, context: .jetpackSite(accountEmail: email)) + return try await authenticator.attemptSignIn( + from: presentingViewController, + context: .jetpackSite(accountEmail: email) + ) } func performInstall() async throws { @@ -246,7 +271,10 @@ class JetpackConnectionService { } guard let authToken else { throw JetpackConnectionError.authenticationFailed } - let _ = try await jetpackConnectionClient.connectUser(wpComAuthentication: .bearer(token: authToken), from: "jetpack-app") + let _ = try await jetpackConnectionClient.connectUser( + wpComAuthentication: .bearer(token: authToken), + from: "jetpack-app" + ) } func performFinalization(account accountID: TaggedManagedObjectID) async throws { From 4d33c9b8d4226684e6b94b8f65db1a20a4ab7352 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 13 May 2026 23:01:21 +1200 Subject: [PATCH 2/2] Refactor WordPressSite to be a struct --- .../WordPressData/Swift/Blog+Features.swift | 2 +- .../WordPressData/Swift/Blog+SelfHosted.swift | 202 ++++++++++++------ .../Services/UserListViewModelTests.swift | 2 +- .../ApplicationPasswordsRepositoryTests.swift | 60 +++++- .../ApplicationPasswordRequiredView.swift | 6 +- .../Login/SelfHostedSiteAuthenticator.swift | 16 +- .../Classes/Networking/WordPressClient.swift | 40 ++-- .../ApplicationPasswordRepository.swift | 32 ++- .../Editor/EditorDependencyManager.swift | 4 +- .../CustomPostTypeService.swift | 5 +- .../Login/JetpackConnectionViewModel.swift | 6 +- 11 files changed, 257 insertions(+), 118 deletions(-) diff --git a/Sources/WordPressData/Swift/Blog+Features.swift b/Sources/WordPressData/Swift/Blog+Features.swift index 577bc896dfd2..b7e88e332ca9 100644 --- a/Sources/WordPressData/Swift/Blog+Features.swift +++ b/Sources/WordPressData/Swift/Blog+Features.swift @@ -109,7 +109,7 @@ extension Blog { // alt is not supported via XML-RPC API // https://core.trac.wordpress.org/ticket/58582 // https://github.com/wordpress-mobile/WordPress-Android/issues/18514#issuecomment-1589752274 - return supportsRestAPI || supportsCoreRestApi + return supportsRestAPI || hasDirectCoreRESTAPIAccess case .contactInfo: return hasRequiredJetpackVersion("8.5") || isHostedAtWPcom case .blockEditorSettings: diff --git a/Sources/WordPressData/Swift/Blog+SelfHosted.swift b/Sources/WordPressData/Swift/Blog+SelfHosted.swift index bd59bd9aa6d2..2b46171c0555 100644 --- a/Sources/WordPressData/Swift/Blog+SelfHosted.swift +++ b/Sources/WordPressData/Swift/Blog+SelfHosted.swift @@ -182,11 +182,11 @@ public extension Blog { self.account == nil } - @objc var supportsCoreRestApi: Bool { - if case .selfHosted = try? WordPressSite(blog: self) { - return true + @objc var hasDirectCoreRESTAPIAccess: Bool { + guard let site = try? WordPressSite(blog: self) else { + return false } - return false + return site.applicationPasswordCredentials != nil } } @@ -198,79 +198,155 @@ public extension WpApiApplicationPasswordDetails { } } -public enum WordPressSite: Hashable { - case dotCom(siteURL: URL, siteId: Int, authToken: String) - case selfHosted( - blogId: TaggedManagedObjectID, - siteURL: URL, - apiRootURL: ParsedUrl, - username: String, - authToken: String - ) - - public init(blog: Blog) throws { +/// Describes a WordPress site's hosting type, authentication credentials, +/// and API capabilities. +/// +/// This is a value type constructed from a `Blog` Core Data object. It captures +/// a snapshot of the site's characteristics at construction time. +/// +/// `WordPressSite` is not a one-to-one mapping with `Blog`. It represents the +/// subset of `Blog` instances that have access to the WordPress core REST API +/// (wp/v2). A self-hosted site that only has XML-RPC credentials is not +/// representable as a `WordPressSite`. +/// +/// - All WordPress.com sites qualify (wp/v2 is accessed via WP.com REST API +/// with OAuth). +/// - Self-hosted sites must have application password credentials. +public struct WordPressSite { + public let blogId: TaggedManagedObjectID + public let siteURL: URL + public let flavor: ApiFlavor + + public init(blogId: TaggedManagedObjectID, siteURL: URL, flavor: ApiFlavor) { + self.blogId = blogId + self.siteURL = siteURL + self.flavor = flavor + } +} + +extension WordPressSite { + public enum ApiFlavor { + /// A site hosted on WordPress.com. Always has OAuth access via + /// WPAccount. May also have application password credentials + /// (e.g., Atomic sites). + case dotCom(DotComCredentials) + + /// A self-hosted WordPress site with application password credentials. + /// Application password is required for wp/v2 API access. + case selfHosted(ApplicationPasswordCredentials) + } +} + +extension WordPressSite { + public struct DotComCredentials: Hashable { + public let siteId: Int + public let oAuthToken: String + /// Non-nil for Atomic sites that also have application password access. + public let applicationPassword: ApplicationPasswordCredentials? + + public init(siteId: Int, oAuthToken: String, applicationPassword: ApplicationPasswordCredentials?) { + self.siteId = siteId + self.oAuthToken = oAuthToken + self.applicationPassword = applicationPassword + } + } + + public struct ApplicationPasswordCredentials: Hashable { + public let apiRootURL: ParsedUrl + public let username: String + public let token: String + + public init(apiRootURL: ParsedUrl, username: String, token: String) { + self.apiRootURL = apiRootURL + self.username = username + self.token = token + } + } +} + +extension WordPressSite: Hashable { + public static func == (lhs: WordPressSite, rhs: WordPressSite) -> Bool { + lhs.blogId == rhs.blogId + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(blogId) + } +} + +extension WordPressSite { + /// Constructs a `WordPressSite` from a `Blog` Core Data object. + /// + /// Throws if the blog lacks enough data to determine its hosting type + /// and at least one valid authentication method. + /// + /// For self-hosted sites, application password credentials are required. + /// Sites without them cannot be represented as a `WordPressSite`. + public init(blog: Blog, keychain: KeychainAccessible = KeychainUtils()) throws { let siteURL = try blog.getUrl() - // Directly access the site content when available. + self.blogId = TaggedManagedObjectID(blog) + self.siteURL = siteURL + + // Build application password credentials if available. + // These are shared across both hosting types — WordPress.com Atomic + // sites can have them too. + let applicationPassword: ApplicationPasswordCredentials? if let restApiRootURL = blog.restApiRootURL, - let restApiRootURL = try? ParsedUrl.parse(input: restApiRootURL), + let parsedApiRoot = try? ParsedUrl.parse(input: restApiRootURL), let username = blog.username, - let authToken = try? blog.getApplicationToken() + let token = try? blog.getApplicationToken(using: keychain) { - self = .selfHosted( - blogId: TaggedManagedObjectID(blog), - siteURL: siteURL, - apiRootURL: restApiRootURL, + applicationPassword = ApplicationPasswordCredentials( + apiRootURL: parsedApiRoot, username: username, - authToken: authToken + token: token ) - } else if let account = blog.account, let siteId = blog.dotComID?.intValue { - // When the site is added via a WP.com account, access the site via WP.com - let authToken = try account.authToken ?? WPAccount.token(forUsername: account.username) - self = .dotCom(siteURL: siteURL, siteId: siteId, authToken: authToken) } else { - // In theory, this branch should never run, because the two if statements above should have covered all paths. - // But we'll keep it here as the fallback. - let url = try blog.getUrl() - let apiRootURL = try ParsedUrl.parse( - input: blog.restApiRootURL ?? blog.getUrl().appending(path: "wp-json").absoluteString - ) - self = .selfHosted( - blogId: TaggedManagedObjectID(blog), - siteURL: url, - apiRootURL: apiRootURL, - username: try blog.getUsername(), - authToken: try blog.getApplicationToken() - ) + applicationPassword = nil } - } - public var siteURL: URL { - switch self { - case let .dotCom(siteURL, _, _): - return siteURL - case let .selfHosted(_, siteURL, _, _, _): - return siteURL + // Check for WordPress.com account first. This means Atomic sites + // (which have both an account and application password credentials) + // resolve to `.dotCom`. + if let account = blog.account, + let siteId = blog.dotComID?.intValue + { + let authToken = + try account.authToken + ?? WPAccount.token(forUsername: account.username) + self.flavor = .dotCom( + DotComCredentials( + siteId: siteId, + oAuthToken: authToken, + applicationPassword: applicationPassword + ) + ) + } else { + // Self-hosted sites must have application password credentials + // for wp/v2 API access. + guard let applicationPassword else { + throw Blog.BlogCredentialsError.blogPasswordMissing + } + self.flavor = .selfHosted(applicationPassword) } } +} - public func blog(in context: NSManagedObjectContext) throws -> Blog? { - switch self { - case let .dotCom(_, siteId, _): - return try Blog.lookup(withID: siteId, in: context) - case let .selfHosted(blogId, _, _, _, _): - return try context.existingObject(with: blogId) +extension WordPressSite { + /// The application password credentials, if available. + /// Always non-nil for self-hosted sites. Optional for WordPress.com sites + /// (non-nil for Atomic sites). + public var applicationPasswordCredentials: ApplicationPasswordCredentials? { + switch flavor { + case let .dotCom(credentials): + return credentials.applicationPassword + case let .selfHosted(credentials): + return credentials } } - public func blogId(in coreDataStack: CoreDataStack) -> TaggedManagedObjectID? { - switch self { - case let .dotCom(_, siteId, _): - return coreDataStack.performQuery { context in - guard let blog = try? Blog.lookup(withID: siteId, in: context) else { return nil } - return TaggedManagedObjectID(blog) - } - case let .selfHosted(id, _, _, _, _): - return id - } + /// Look up the `Blog` object in a given Core Data context. + public func blog(in context: NSManagedObjectContext) throws -> Blog { + try context.existingObject(with: blogId) } } diff --git a/Tests/KeystoneTests/Tests/Services/UserListViewModelTests.swift b/Tests/KeystoneTests/Tests/Services/UserListViewModelTests.swift index 8ac54ee3fec4..78a43392e8aa 100644 --- a/Tests/KeystoneTests/Tests/Services/UserListViewModelTests.swift +++ b/Tests/KeystoneTests/Tests/Services/UserListViewModelTests.swift @@ -24,7 +24,7 @@ class UserListViewModelTests: XCTestCase { ), authentication: .none ) - let client = try WordPressClient( + let client = WordPressClient( api: api, siteURL: URL(string: "https://example.com")! ) diff --git a/Tests/KeystoneTests/Tests/Utility/ApplicationPasswordsRepositoryTests.swift b/Tests/KeystoneTests/Tests/Utility/ApplicationPasswordsRepositoryTests.swift index 8f962d6c059f..31831ed20f2a 100644 --- a/Tests/KeystoneTests/Tests/Utility/ApplicationPasswordsRepositoryTests.swift +++ b/Tests/KeystoneTests/Tests/Utility/ApplicationPasswordsRepositoryTests.swift @@ -54,6 +54,27 @@ class ApplicationPasswordsRepositoryTests { #expect(password == "abcd efgh") } + @Test + func atomicSiteWithExistingApplicationPassword() async throws { + defer { HTTPStubs.removeAllStubs() } + + try await signInWPComAccount() + let blog = try await createAtomicSite(existingApplicationPassword: "existing token") + + let validationMonitor = Monitor() + let creationMonitor = Monitor() + stubCurrentApplicationPassword(host: "atomic.com", monitor: validationMonitor) + stubJetpackProxyCreateApplicationPassword(siteId: 456, password: "new token", monitor: creationMonitor) + + let repository = ApplicationPasswordRepository.forTesting(coreDataStack: coreDataStack, keychain: keychain) + try await repository.createPasswordIfNeeded(for: blog) + + let password = await password(of: blog) + #expect(password == "existing token") + #expect(validationMonitor.numberOfRequests > 0) + #expect(creationMonitor.numberOfRequests == 0) + } + @Test func selfHostedSite() async throws { defer { HTTPStubs.removeAllStubs() } @@ -287,7 +308,7 @@ private extension ApplicationPasswordsRepositoryTests { } } - func createAtomicSite() async throws -> TaggedManagedObjectID { + func createAtomicSite(existingApplicationPassword: String? = nil) async throws -> TaggedManagedObjectID { try await coreDataStack.performAndSave { context in let account = try #require(try WPAccount.lookupDefaultWordPressComAccount(in: context)) let blog = try BlogBuilder(context, dotComID: 456) @@ -295,6 +316,11 @@ private extension ApplicationPasswordsRepositoryTests { .withAccount(id: account.objectID) .with(atomic: true) .build() + if let existingApplicationPassword { + blog.username = "demo" + blog.restApiRootURL = "https://atomic.com/wp-json" + try blog.setApplicationToken(existingApplicationPassword, using: self.keychain) + } return TaggedManagedObjectID(blog) } } @@ -353,9 +379,11 @@ private extension ApplicationPasswordsRepositoryTests { } } - func stubJetpackProxyCreateApplicationPassword(siteId: Int, password: String) { + func stubJetpackProxyCreateApplicationPassword(siteId: Int, password: String, monitor: Monitor? = nil) { stub(condition: isHost("public-api.wordpress.com") && isPath("/rest/v1.1/jetpack-blogs/\(siteId)/rest-api")) { _ in + monitor?.requestReceived() + let json = """ { "data": { @@ -383,6 +411,34 @@ private extension ApplicationPasswordsRepositoryTests { } } + func stubCurrentApplicationPassword(host: String, monitor: Monitor? = nil) { + stub(condition: isHost(host) && isPath("/wp-json/wp/v2/users/me/application-passwords/introspect")) { _ in + monitor?.requestReceived() + + let json = """ + { + "uuid": "56cadaa8-e810-4752-abf9-cc39e120ea97", + "app_id": "", + "name": "Test", + "created": "2025-07-15T22:14:13", + "last_used": "2025-07-25T02:43:58", + "last_ip": "127.0.0.1", + "_links": { + "self": [ + { + "href": "https://\(host)/wp-json/wp/v2/users/1/application-passwords/56cadaa8-e810-4752-abf9-cc39e120ea97", + "targetHints": { + "allow": ["GET", "POST", "PUT", "PATCH", "DELETE"] + } + } + ] + } + } + """ + return HTTPStubsResponse(data: json.data(using: .utf8)!, statusCode: 200, headers: nil) + } + } + func stubWPComWpV2GetUser(siteId: Int) { stub(condition: isHost("public-api.wordpress.com") && isPath("/wp/v2/sites/\(siteId)/users/me")) { _ in let json = """ diff --git a/WordPress/Classes/Login/ApplicationPasswordRequiredView.swift b/WordPress/Classes/Login/ApplicationPasswordRequiredView.swift index 213a58c01c14..0ac0d6870097 100644 --- a/WordPress/Classes/Login/ApplicationPasswordRequiredView.swift +++ b/WordPress/Classes/Login/ApplicationPasswordRequiredView.swift @@ -136,9 +136,9 @@ struct ApplicationPasswordRequiredView: View { } private func updateSite() { - // We check that the site is `selfHosted` to ensure an _Application Password_ is available. That's what this view - // is for, after all. - if let site = try? WordPressSite(blog: blog), case .selfHosted = site { + // We check that the site has application password credentials to ensure + // direct wp/v2 API access is available. That's what this view is for. + if let site = try? WordPressSite(blog: blog), site.applicationPasswordCredentials != nil { self.site = site } } diff --git a/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift b/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift index 73ee00044024..3b47a474f587 100644 --- a/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift +++ b/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift @@ -199,7 +199,7 @@ struct SelfHostedSiteAuthenticator { let result = try await handle( credentials: credentials, apiRootURL: apiRootURL, - apiDetails: details.apiDetails, + apiDiscovery: details, context: context ) trackSuccess(url: details.parsedSiteUrl.url()) @@ -279,7 +279,7 @@ struct SelfHostedSiteAuthenticator { private func handle( credentials: WpApiApplicationPasswordDetails, apiRootURL: URL, - apiDetails: WpApiDetails, + apiDiscovery: AutoDiscoveryAttemptSuccess, context: SignInContext ) async throws(SignInError) -> TaggedManagedObjectID { SVProgressHUD.show() @@ -294,7 +294,7 @@ struct SelfHostedSiteAuthenticator { let blog = try await createSite( credentials: credentials, apiRootURL: apiRootURL, - apiDetails: apiDetails, + apiDiscovery: apiDiscovery, context: context ) @@ -345,7 +345,7 @@ struct SelfHostedSiteAuthenticator { private func createSite( credentials: WpApiApplicationPasswordDetails, apiRootURL: URL, - apiDetails: WpApiDetails, + apiDiscovery: AutoDiscoveryAttemptSuccess, context: SignInContext ) async throws(SignInError) -> TaggedManagedObjectID { // We still need to set the `Blog.xmlrpc`, because it's used all across the app. @@ -359,8 +359,8 @@ struct SelfHostedSiteAuthenticator { let api = WordPressAPI( urlSession: URLSession(configuration: .ephemeral), siteInfo: .selfHosted( - siteUrl: try! ParsedUrl.parse(input: credentials.siteUrl), - apiRoot: try! ParsedUrl.parse(input: apiRootURL.absoluteString) + siteUrl: apiDiscovery.parsedSiteUrl, + apiRoot: apiDiscovery.apiRootUrl ), authentication: WpAuthentication(username: credentials.userLogin, password: credentials.password) ) @@ -429,12 +429,12 @@ struct SelfHostedSiteAuthenticator { blog.setValue(timezone, forOption: "timezone") } - if blog.getOptionString(name: "gmt_offset") == nil, let offset = apiDetails.gmtOffset() { + if blog.getOptionString(name: "gmt_offset") == nil, let offset = apiDiscovery.apiDetails.gmtOffset() { blog.setValue(offset, forOption: "gmt_offset") } if blog.getOptionString(name: "home_url") == nil { - blog.setValue(apiDetails.homeUrlString(), forOption: "home_url") + blog.setValue(apiDiscovery.apiDetails.homeUrlString(), forOption: "home_url") } } diff --git a/WordPress/Classes/Networking/WordPressClient.swift b/WordPress/Classes/Networking/WordPressClient.swift index 5f37c335128a..10052a6c795f 100644 --- a/WordPress/Classes/Networking/WordPressClient.swift +++ b/WordPress/Classes/Networking/WordPressClient.swift @@ -37,7 +37,7 @@ extension WordPressClient { .init("WordPressClient.requestedWithInvalidAuthenticationNotification") } - fileprivate convenience init(site: WordPressSite) { + fileprivate init(site: WordPressSite) { // Currently, the app supports both account passwords and application passwords. // When a site is initially signed in with an account password, WordPress login cookies are stored // in `URLSession.shared`. After switching the site to application password authentication, @@ -55,15 +55,12 @@ extension WordPressClient { coreDataStack: ContextManager.shared ) ) - let siteURL: URL let siteInfo: SiteInfo - switch site { - case let .dotCom(url, siteId, _): - siteURL = url - siteInfo = .wordPressCom(siteId: WpComSiteId(siteId)) - case let .selfHosted(_, url, apiRoot, _, _): - siteURL = url - siteInfo = .selfHosted(siteUrl: try! ParsedUrl.from(url: url), apiRoot: apiRoot) + switch site.flavor { + case let .dotCom(credentials): + siteInfo = .wordPressCom(siteId: WpComSiteId(credentials.siteId)) + case let .selfHosted(credentials): + siteInfo = .selfHosted(siteUrl: try! ParsedUrl.from(url: site.siteURL), apiRoot: credentials.apiRootURL) } let api = WordPressAPI( urlSession: session, @@ -72,7 +69,7 @@ extension WordPressClient { authenticationProvider: provider, appNotifier: notifier, ) - self.init(api: api, siteURL: siteURL) + self.init(api: api, siteURL: site.siteURL) } func installJetpack() async throws -> PluginWithEditContext { @@ -107,11 +104,11 @@ private final class AutoUpdateAuthenticationProvider: @unchecked Sendable, WpDyn self.site = site self.coreDataStack = coreDataStack self.authentication = - switch site { - case let .dotCom(_, _, authToken): - .bearer(token: authToken) - case let .selfHosted(_, _, _, username, authToken): - .init(username: username, password: authToken) + switch site.flavor { + case let .dotCom(credentials): + .bearer(token: credentials.oAuthToken) + case let .selfHosted(credentials): + .init(username: credentials.username, password: credentials.token) } self.cancellable = NotificationCenter.default @@ -146,7 +143,7 @@ private final class AutoUpdateAuthenticationProvider: @unchecked Sendable, WpDyn } func refresh() async -> Bool { - guard let blogId = site.blogId(in: coreDataStack) else { return false } + let blogId = site.blogId do { DDLogInfo("Create a new application password") @@ -172,25 +169,24 @@ private class AppNotifier: @unchecked Sendable, WpAppNotifier { } func requestedWithInvalidAuthentication(requestUrl: String) async { - let blogId = site.blogId(in: coreDataStack) NotificationCenter.default.post( name: WordPressClient.requestedWithInvalidAuthenticationNotification, - object: blogId + object: site.blogId ) } } private extension WordPressSite { func authentication(in context: NSManagedObjectContext) -> WpAuthentication { - switch self { - case let .dotCom(_, siteId, _): - guard let blog = try? Blog.lookup(withID: siteId, in: context), + switch self.flavor { + case .dotCom: + guard let blog = try? context.existingObject(with: blogId), let token = blog.account?.authToken else { return WpAuthentication.none } return WpAuthentication.bearer(token: token) - case let .selfHosted(blogId, _, _, _, _): + case .selfHosted: guard let blog = try? context.existingObject(with: blogId), let username = try? blog.getUsername(), let password = try? blog.getApplicationToken() diff --git a/WordPress/Classes/Services/ApplicationPasswordRepository.swift b/WordPress/Classes/Services/ApplicationPasswordRepository.swift index 03f5c7e02fdc..2a7baa715d9d 100644 --- a/WordPress/Classes/Services/ApplicationPasswordRepository.swift +++ b/WordPress/Classes/Services/ApplicationPasswordRepository.swift @@ -54,33 +54,37 @@ actor ApplicationPasswordRepository { /// The application password was stored via `Blog.setApplicationToken`, not in the `ApplicationPasswordStorage`. /// We want to copy the `Blog.getApplicationToken` to `ApplicationPasswordStorage` if needed. func saveApplicationPassword(of blogId: TaggedManagedObjectID) async throws { + let keychain = await storage.keychain let (owners, site) = try await coreDataStack.performQuery { context in let blog = try context.existingObject(with: blogId) - return (blog.asApplicationPasswordOwners(), try? WordPressSite(blog: blog)) + return ( + blog.asApplicationPasswordOwners(), + try? WordPressSite(blog: blog, keychain: keychain) + ) } - guard case let .selfHosted(_, siteURL, apiRootURL, username, authToken) = site else { + guard let site, let credentials = site.applicationPasswordCredentials else { return } let alreadyStored = await storage .passwords(belongTo: owners) - .contains { $0.password == authToken } + .contains { $0.password == credentials.token } guard !alreadyStored else { return } // No need to propagate the API request error. let api = WordPressAPI( urlSession: URLSession(configuration: .ephemeral), notifyingDelegate: PulseNetworkLogger(), - siteInfo: .selfHosted(siteUrl: try! ParsedUrl.from(url: siteURL), apiRoot: apiRootURL), - authentication: .init(username: username, password: authToken) + siteInfo: .selfHosted(siteUrl: try .from(url: site.siteURL), apiRoot: credentials.apiRootURL), + authentication: .init(username: credentials.username, password: credentials.token) ) guard let uuid = try? await api.applicationPasswords.retrieveCurrentWithViewContext().data.uuid.uuid else { return } - try await storage.save(.init(password: .init(uuid: uuid, password: authToken), owners: owners)) + try await storage.save(.init(password: .init(uuid: uuid, password: credentials.token), owners: owners)) } /// When returning true, a valid application password is guaranteed to be returned by the `Blog.getApplicationToken` function. @@ -160,7 +164,7 @@ private extension ApplicationPasswordRepository { let blog = try context.existingObject(with: blogId) return try ( blog.asApplicationPasswordOwners(), - blog.getUrlString(), + blog.getUrl(), ) } let passwords = await storage.passwords(belongTo: owners) @@ -171,7 +175,7 @@ private extension ApplicationPasswordRepository { let session = URLSession(configuration: .ephemeral) var validPasswords = [ApplicationPassword]() var invalidPasswords = [ApplicationPassword]() - let parsedSiteURL = try ParsedUrl.parse(input: siteUrl) + let parsedSiteURL = try ParsedUrl.from(url: siteUrl) for password in passwords { let api = WordPressAPI( urlSession: session, @@ -374,7 +378,17 @@ private extension ApplicationPasswordRepository { if let username { siteUsername = username } else if let dotComId, let dotComAuthToken { - let site = WordPressSite.dotCom(siteURL: siteURL, siteId: dotComId.intValue, authToken: dotComAuthToken) + let site = WordPressSite( + blogId: blogId, + siteURL: siteURL, + flavor: .dotCom( + WordPressSite.DotComCredentials( + siteId: dotComId.intValue, + oAuthToken: dotComAuthToken, + applicationPassword: nil + ) + ) + ) let client = WordPressClientFactory.shared.instance(for: site) siteUsername = try await client.api.users.retrieveMeWithEditContext().data.username try await coreDataStack.performAndSave { context in diff --git a/WordPress/Classes/Utility/Editor/EditorDependencyManager.swift b/WordPress/Classes/Utility/Editor/EditorDependencyManager.swift index 290c7daba28f..6abbb282d7ed 100644 --- a/WordPress/Classes/Utility/Editor/EditorDependencyManager.swift +++ b/WordPress/Classes/Utility/Editor/EditorDependencyManager.swift @@ -284,8 +284,8 @@ final class EditorDependencyManager: Sendable { var siteId: Int? = nil - if case .dotCom(_, let _siteId, _) = site { - siteId = _siteId + if case let .dotCom(credentials) = site.flavor { + siteId = credentials.siteId } let hasBlockTheme = try await client.supports(.blockTheme, forSiteId: siteId) diff --git a/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostTypeService.swift b/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostTypeService.swift index 43f583787502..2167025653f6 100644 --- a/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostTypeService.swift +++ b/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostTypeService.swift @@ -17,10 +17,7 @@ class CustomPostTypeService { } init?(blog: Blog) { - guard FeatureFlag.customPostTypes.enabled, - let site = try? WordPressSite(blog: blog), - case .selfHosted = site - else { return nil } + guard FeatureFlag.customPostTypes.enabled, let site = try? WordPressSite(blog: blog) else { return nil } self.blog = TaggedManagedObjectID(blog) self.client = WordPressClientFactory.shared.instance(for: site) } diff --git a/WordPress/Classes/ViewRelated/Jetpack/Login/JetpackConnectionViewModel.swift b/WordPress/Classes/ViewRelated/Jetpack/Login/JetpackConnectionViewModel.swift index 5add606f7543..178b50ff61ac 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Login/JetpackConnectionViewModel.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Login/JetpackConnectionViewModel.swift @@ -206,7 +206,7 @@ class JetpackConnectionService { } guard let site = try? WordPressSite(blog: blog), - case let .selfHosted(_, _, apiRootURL, username, password) = site + case let .selfHosted(credentials) = site.flavor else { return nil } @@ -214,9 +214,9 @@ class JetpackConnectionService { self.blogId = TaggedManagedObjectID(blog) self.client = WordPressClientFactory.shared.instance(for: site) self.jetpackConnectionClient = .init( - apiRootUrl: apiRootURL, + apiRootUrl: credentials.apiRootURL, urlSession: .init(configuration: .ephemeral), - authentication: .init(username: username, password: password) + authentication: .init(username: credentials.username, password: credentials.token) ) }