Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Modules/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ let package = Package(
.package(url: "https://github.com/wordpress-mobile/WordPressKit-iOS", branch: "wpios-edition"),
.package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"),
// We can't use wordpress-rs branches nor commits here. Only tags work.
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20250127"),
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20250409"),
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", revision: "a03e0dae10a404c88c215bfcee3176df951302f5"),
.package(url: "https://github.com/Automattic/color-studio", branch: "trunk"),
.package(url: "https://github.com/wordpress-mobile/AztecEditor-iOS", from: "1.20.0"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ public typealias InMemoryPluginDirectoryDataStore = InMemoryDataStore<PluginInfo

extension PluginDirectoryDataStoreQuery {
public static func slug(_ slug: PluginWpOrgDirectorySlug) -> PluginDirectoryDataStoreQuery {
.init(sortBy: KeyPathComparator(\.name)) { $0.slug == slug.slug }
.init(sortBy: KeyPathComparator(\.name)) { $0.slug == slug }
}
}

extension PluginInformation: @retroactive Identifiable {
public var id: PluginWpOrgDirectorySlug {
PluginWpOrgDirectorySlug(slug: slug)
slug
}
}

Expand Down
2 changes: 1 addition & 1 deletion Modules/Sources/WordPressCore/Plugins/PluginService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public actor PluginService: PluginServiceProtocol {
self.client = client
self.wordpressCoreVersion = wordpressCoreVersion
self.urlSession = URLSession(configuration: .ephemeral)
wpOrgClient = WordPressOrgApiClient(requestExecutor: urlSession)
wpOrgClient = WordPressOrgApiClient(urlSession: urlSession)
}

public func fetchInstalledPlugins() async throws {
Expand Down
6 changes: 3 additions & 3 deletions WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,8 @@ class ApplicationTokenListViewModel: ObservableObject {
(lhs.lastUsed ?? .distantPast, lhs.createdAt) > (rhs.lastUsed ?? .distantPast, rhs.createdAt)
}
self.applicationTokens = tokens
} catch let error as WpApiError {
self.errorMessage = error.errorMessage
} catch {
self.errorMessage = SharedStrings.Error.generic
self.errorMessage = error.localizedDescription
}
}
}
Expand Down
22 changes: 0 additions & 22 deletions WordPress/Classes/Extensions/WpApiError+Localized.swift

This file was deleted.

72 changes: 2 additions & 70 deletions WordPress/Classes/Login/LoginWithUrlView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,11 @@ struct LoginWithUrlView: View {
// https://github.com/swiftlang/swift/issues/76807
func login() async {
do {
let anchor = self.anchor ?? UIWindow()
let credentials = try await client.signIn(site: urlField, from: anchor)
self.loginCompleted(credentials)
} catch {
errorMessage = error.errorMessage
errorMessage = error.localizedDescription
}

isLoading = false
Expand All @@ -108,75 +109,6 @@ private extension LoginWithUrlView {
static var enterSiteAddress: String { NSLocalizedString("addSite.selfHosted.enterSiteAddress", value: "Enter the address of the WordPress site you'd like to connect.", comment: "A message to inform users to type the site address in the text field.") }
}

private extension SelfHostedSiteAuthenticator.SignInError {

var errorMessage: String? {
switch self {
case let .authentication(error):
return error.errorMessage
case .loadingSiteInfoFailure:
return NSLocalizedString("addSite.selfHosted.loadingSiteInfoFailure", value: "Cannot load the WordPress site details", comment: "Error message shown when failing to load details from a self-hosted WordPress site")
case .savingSiteFailure:
return NSLocalizedString("addSite.selfHosted.savingSiteFailure", value: "Cannot save the WordPress site, please try again later.", comment: "Error message shown when failing to save a self-hosted site to user's device")
}
}

}

extension WordPressLoginClientError {

var errorMessage: String? {
switch self {
case let .invalidSiteAddress(error):
return error.errorMessage
case .missingLoginUrl:
return NSLocalizedString("addSite.selfHosted.noLoginUrlFound", value: "Application Password authentication needs to be enabled on the WordPress site.", comment: "Error message shown when application-password authentication is disabled on a self-hosted WordPress site")
case .cancelled:
return nil
case .authenticationError, .invalidApplicationPasswordCallback:
return NSLocalizedString("addSite.selfHosted.authenticationFailed", value: "Cannot login using Application Password authentication.", comment: "Error message shown when an receiving an invalid application-password authentication result from a self-hosted WordPress site")
case .unknown:
return NSLocalizedString("addSite.selfHosted.unknownError", value: "Something went wrong. Please try again.", comment: "Error message when an unknown error occurred when adding a self-hosted site")
}
}

}

// MARK: - WordPressAPI helpers

private extension UrlDiscoveryError {
var errorMessage: String? {
let errors: [UrlDiscoveryAttemptError]

switch self {
case let .UrlDiscoveryFailed(attempts):
errors = attempts.values.compactMap {
switch $0 {
case let .failure(failure):
return failure
case .success:
return nil
}
}
}

let notWordPressSite = errors.contains {
switch $0 {
case .fetchApiRootUrlFailed, .fetchApiDetailsFailed:
return true
case .failedToParseSiteUrl:
return false
}
}

if notWordPressSite {
return NSLocalizedString("addSite.restApiNotAvailable", value: "The site at this address is not a WordPress site. For us to connect to it, the site must use WordPress.", comment: "Error message shown a URL does not point to an existing site.")
}

return NSLocalizedString("addSite.selfHosted.invalidUrl", value: "The site address is not valid.", comment: "Error message when user input is not a WordPress site")
}
}

// MARK: - SwiftUI Preview

#Preview {
Expand Down
70 changes: 60 additions & 10 deletions WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,29 @@ import WordPressShared

final actor SelfHostedSiteAuthenticator {

enum SignInError: Error {
case authentication(WordPressLoginClientError)
private static let callbackURL = URL(string: "x-wordpress-app://login-callback")!

enum SignInError: Error, LocalizedError {
case authentication(Error)
case loadingSiteInfoFailure
case savingSiteFailure
case invalidApplicationPasswordCallback
case cancelled

var errorDescription: String? {
switch self {
case .authentication(let error):
return error.localizedDescription
case .loadingSiteInfoFailure:
return NSLocalizedString("addSite.selfHosted.loadingSiteInfoFailure", value: "Cannot load the WordPress site details", comment: "Error message shown when failing to load details from a self-hosted WordPress site")
case .savingSiteFailure:
return NSLocalizedString("addSite.selfHosted.savingSiteFailure", value: "Cannot save the WordPress site, please try again later.", comment: "Error message shown when failing to save a self-hosted site to user's device")
case .invalidApplicationPasswordCallback:
return NSLocalizedString("addSite.selfHosted.authenticationFailed", value: "Cannot login using Application Password authentication.", comment: "Error message shown when an receiving an invalid application-password authentication result from a self-hosted WordPress site")
case .cancelled:
return nil
}
}
}

private let internalClient: WordPressLoginClient
Expand All @@ -39,7 +58,7 @@ final actor SelfHostedSiteAuthenticator {
}

@MainActor
func signIn(site: String, from anchor: ASPresentationAnchor?) async throws(SignInError) -> WordPressOrgCredentials {
func signIn(site: String, from anchor: ASPresentationAnchor) async throws(SignInError) -> WordPressOrgCredentials {
do {
let result = try await _signIn(site: site, from: anchor)
await trackSuccess(url: site)
Expand All @@ -51,10 +70,12 @@ final actor SelfHostedSiteAuthenticator {
}

@MainActor
private func _signIn(site: String, from anchor: ASPresentationAnchor?) async throws(SignInError) -> WordPressOrgCredentials {
private func _signIn(site: String, from anchor: ASPresentationAnchor) async throws(SignInError) -> WordPressOrgCredentials {
let success: WpApiApplicationPasswordDetails
do {
success = try await authentication(site: site, from: anchor)
} catch let error as SignInError {
throw error
} catch {
throw .authentication(error)
}
Expand All @@ -63,7 +84,7 @@ final actor SelfHostedSiteAuthenticator {
}

@MainActor
func authentication(site: String, from anchor: ASPresentationAnchor?) async throws(WordPressLoginClientError) -> WpApiApplicationPasswordDetails {
func authentication(site: String, from anchor: ASPresentationAnchor) async throws -> WpApiApplicationPasswordDetails {
let appId: WpUuid
let appName: String

Expand All @@ -79,11 +100,40 @@ final actor SelfHostedSiteAuthenticator {
let timestamp = ISO8601DateFormatter.string(from: .now, timeZone: .current, formatOptions: .withInternetDateTime)
let appNameValue = "\(appName) - \(deviceName) (\(timestamp))"

return try await internalClient.login(
site: site,
appName: appNameValue,
appId: appId
)
let loginURL = try await internalClient.loginURL(forSite: site, application: .init(id: appId, name: appNameValue, callbackUrl: SelfHostedSiteAuthenticator.callbackURL.absoluteString))
let callback = try await authenticate(url: loginURL, callbackURL: SelfHostedSiteAuthenticator.callbackURL, from: anchor)
return try internalClient.credentials(from: callback)
}

@MainActor
func authenticate(url: URL, callbackURL: URL, from anchor: ASPresentationAnchor) async throws -> URL {
let provider = WebAuthenticationPresentationAnchorProvider(anchor: anchor)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can actually just pass in ASPresentationAnchor() here – no need to pass something down the whole hierarchy

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was this crash, which I couldn't reproduce. I suspect the crash was due to using a new window instance. So I try to pass an existing window down to the web authentication session instance, instead of creating a new one.

return try await withCheckedThrowingContinuation { continuation in
let session = ASWebAuthenticationSession(
url: url,
callbackURLScheme: callbackURL.scheme!
) { url, error in
if let url {
continuation.resume(returning: url)
} else if let error = error as? ASWebAuthenticationSessionError {
switch error.code {
case .canceledLogin:
assertionFailure("An unexpected error received: \(error)")
continuation.resume(throwing: SignInError.cancelled)
case .presentationContextInvalid, .presentationContextNotProvided:
assertionFailure("An unexpected error received: \(error)")
continuation.resume(throwing: SignInError.cancelled)
@unknown default:
assertionFailure("An unexpected error received: \(error)")
continuation.resume(throwing: SignInError.cancelled)
}
} else {
continuation.resume(throwing: SignInError.invalidApplicationPasswordCallback)
}
}
session.presentationContextProvider = provider
session.start()
}
}

private func handleSuccess(_ success: WpApiApplicationPasswordDetails) async throws(SignInError) -> WordPressOrgCredentials {
Expand Down
8 changes: 6 additions & 2 deletions WordPress/Classes/Networking/WordPressClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,17 @@ extension WordPressClient {

init(site: WordPressSite) {
// `site.barUrl` is a legal HTTP URL, which should be convertable to the `ParsedUrl` type.
// TODO: Read the "api root url" from storage.
let parsedUrl: ParsedUrl
do {
parsedUrl = try ParsedUrl.parse(input: site.baseUrl.absoluteString)
} catch {
fatalError("Failed to cast URL (\(site.baseUrl.absoluteString)) to ParsedUrl: \(error)")
}

let url = parsedUrl.asURL().appending(path: "wp-json")
let apiRootUrl = try! ParsedUrl.parse(input: url.absoluteString)

// 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,
Expand All @@ -53,10 +57,10 @@ extension WordPressClient {

switch site.type {
case let .dotCom(authToken):
let api = WordPressAPI(urlSession: session, baseUrl: parsedUrl, authenticationStategy: .authorizationHeader(token: authToken))
let api = WordPressAPI(urlSession: session, apiRootUrl: apiRootUrl, authenticationStategy: .authorizationHeader(token: authToken))
self.init(api: api, rootUrl: parsedUrl)
case .selfHosted(let username, let authToken):
let api = WordPressAPI.init(urlSession: session, baseUrl: parsedUrl, authenticationStategy: .init(username: username, password: authToken))
let api = WordPressAPI.init(urlSession: session, apiRootUrl: apiRootUrl, authenticationStategy: .init(username: username, password: authToken))
self.init(api: api, rootUrl: parsedUrl)
}
}
Expand Down
4 changes: 2 additions & 2 deletions WordPress/Classes/Plugins/Views/AddNewPluginView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ private class AddNewPluginViewModel: ObservableObject {
if plugins.isEmpty {
sections = [.searchResult(rows: [.empty])]
} else {
sections = [.searchResult(rows: plugins.map { .plugin($0, isInstalled: installedPlugins.contains(PluginWpOrgDirectorySlug(slug: $0.slug))) })]
sections = [.searchResult(rows: plugins.map { .plugin($0, isInstalled: installedPlugins.contains($0.slug)) })]
}
update(to: sections, ifStillIn: expectedMode)
} catch {
Expand All @@ -338,7 +338,7 @@ private class AddNewPluginViewModel: ObservableObject {
if plugins.plugins.isEmpty {
return .plugins(category: category, rows: [.empty])
} else {
return .plugins(category: category, rows: plugins.plugins.map { .plugin($0, isInstalled: installedPlugins.contains(PluginWpOrgDirectorySlug(slug: $0.slug))) })
return .plugins(category: category, rows: plugins.plugins.map { .plugin($0, isInstalled: installedPlugins.contains($0.slug)) })
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ private final class InstalledPluginsListViewModel: ObservableObject {
do {
try await self.service.fetchInstalledPlugins()
} catch {
self.error = (error as? WpApiError)?.errorMessage ?? error.localizedDescription
self.error = error.localizedDescription
}
}

Expand Down Expand Up @@ -256,7 +256,7 @@ private final class InstalledPluginsListViewModel: ObservableObject {
.sorted(using: KeyPathComparator(\ListSection.filter.rawValue))
case let .failure(error):
self.showNoPluginsView = false
self.error = (error as? WpApiError)?.errorMessage ?? error.localizedDescription
self.error = error.localizedDescription
}
}

Expand Down
Loading