Skip to content

Commit 5ae3636

Browse files
committed
Add a RestApiUpgradePrompt for migrating site to application passwords
1 parent 6e52a41 commit 5ae3636

File tree

3 files changed

+137
-7
lines changed

3 files changed

+137
-7
lines changed

Modules/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ let package = Package(
6060
.target(name: "WordPressFlux"),
6161
.target(name: "WordPressSharedObjC", resources: [.process("Resources")]),
6262
.target(name: "WordPressShared", dependencies: [.target(name: "WordPressSharedObjC")], resources: [.process("Resources")]),
63-
.target(name: "WordPressUI", resources: [.process("Resources")]),
63+
.target(name: "WordPressUI", dependencies: [.target(name: "DesignSystem")], resources: [.process("Resources")]),
6464
.testTarget(name: "JetpackStatsWidgetsCoreTests", dependencies: [.target(name: "JetpackStatsWidgetsCore")]),
6565
.testTarget(name: "DesignSystemTests", dependencies: [.target(name: "DesignSystem")]),
6666
.testTarget(name: "WordPressFluxTests", dependencies: ["WordPressFlux"]),
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import SwiftUI
2+
import DesignSystem
3+
4+
public struct RestApiUpgradePrompt: View {
5+
6+
private let localizedFeatureName: String
7+
private var didTapGetStarted: () -> Void
8+
9+
public init(localizedFeatureName: String, didTapGetStarted: @escaping () -> Void) {
10+
self.localizedFeatureName = localizedFeatureName
11+
self.didTapGetStarted = didTapGetStarted
12+
}
13+
14+
public var body: some View {
15+
VStack {
16+
let scrollView = ScrollView {
17+
VStack(alignment: .leading) {
18+
Text(Strings.title)
19+
.font(.largeTitle)
20+
.fontWeight(.semibold)
21+
.padding(.bottom)
22+
23+
Text(Strings.description(localizedFeatureName: localizedFeatureName))
24+
.font(.body)
25+
}.padding()
26+
}
27+
28+
if #available(iOS 16.4, *) {
29+
scrollView.scrollBounceBehavior(.basedOnSize, axes: [.vertical])
30+
}
31+
32+
Spacer()
33+
VStack {
34+
Button(action: didTapGetStarted, label: {
35+
HStack {
36+
Spacer()
37+
Text("Get Started")
38+
.font(.headline)
39+
.padding(.DS.Padding.half)
40+
Spacer()
41+
}
42+
}).buttonStyle(.borderedProminent)
43+
}.padding()
44+
}
45+
}
46+
47+
private enum Strings {
48+
static var title: String {
49+
NSLocalizedString("applicationPasswordRequired.title", value: "Application Password Required", comment: "Title for the prompt to upgrade to Application Passwords")
50+
}
51+
52+
static func description(localizedFeatureName: String) -> String {
53+
let format = NSLocalizedString("applicationPasswordRequired.description", value: "Application passwords are a more secure way to connect to your self-hosted site, and enable support for features like %@.", comment: "Description for the prompt to upgrade to Application Passwords. The first argument is the name of the feature that requires Application Passwords.")
54+
return String(format: format, localizedFeatureName)
55+
}
56+
}
57+
}
58+
59+
#Preview {
60+
RestApiUpgradePrompt(localizedFeatureName: "User Management") {
61+
debugPrint("Tapped Get Started")
62+
}
63+
}

WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+SectionHelpers.swift

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import Foundation
22
import UIKit
33
import SwiftUI
4+
import WordPressUI
5+
import WordPressAPI
46

57
extension Array where Element: BlogDetailsSection {
68
fileprivate func findSectionIndex(of category: BlogDetailsSectionCategory) -> Int? {
@@ -120,7 +122,7 @@ extension BlogDetailsViewController {
120122

121123
@objc func shouldShowApplicationPasswordRow() -> Bool {
122124
// Only available for application-password authenticated self-hosted sites.
123-
return self.blog.account == nil && self.blog.userID != nil && (try? WordPressSite(blog: self.blog)) != nil
125+
return self.blog.account == nil && self.blog.userID != nil
124126
}
125127

126128
private func createApplicationPasswordService() -> ApplicationPasswordService? {
@@ -139,13 +141,17 @@ extension BlogDetailsViewController {
139141
}
140142

141143
@objc func showApplicationPasswordManagement() {
142-
guard let presentationDelegate, let service = createApplicationPasswordService() else {
144+
guard let presentationDelegate, let userId = self.blog.userID?.intValue else {
143145
return
144146
}
145147

146-
let viewModel = ApplicationTokenListViewModel(dataProvider: service)
147-
let viewController = UIHostingController(rootView: ApplicationTokenListView(viewModel: viewModel))
148-
presentationDelegate.presentBlogDetailsViewController(viewController)
148+
let feature = NSLocalizedString("applicationPasswordRequired.feature.plugins", value: "Application Passwords Management", comment: "Feature name for managing Application Passwords in the app")
149+
let rootView = ApplicationPasswordRequiredView(blog: self.blog, localizedFeatureName: feature) { client in
150+
let service = ApplicationPasswordService(api: client, currentUserId: userId)
151+
let viewModel = ApplicationTokenListViewModel(dataProvider: service)
152+
return ApplicationTokenListView(viewModel: viewModel)
153+
}
154+
presentationDelegate.presentBlogDetailsViewController(UIHostingController(rootView: rootView))
149155
}
150156

151157
@objc func showManagePluginsScreen() {
@@ -157,4 +163,65 @@ extension BlogDetailsViewController {
157163
let listViewController = PluginListViewController(site: site, query: query)
158164
presentationDelegate?.presentBlogDetailsViewController(listViewController)
159165
}
160-
}
166+
}
167+
168+
struct ApplicationPasswordRequiredView<Content: View>: View {
169+
private let blog: Blog
170+
private let localizedFeatureName: String
171+
@State private var site: WordPressSite?
172+
private let builder: (WordPressClient) -> Content
173+
174+
init(blog: Blog, localizedFeatureName: String, @ViewBuilder content: @escaping (WordPressClient) -> Content) {
175+
wpAssert(blog.account == nil, "The Blog argument should be a self-hosted site")
176+
177+
self.blog = blog
178+
self.localizedFeatureName = localizedFeatureName
179+
self.site = try? WordPressSite(blog: blog)
180+
self.builder = content
181+
}
182+
183+
var body: some View {
184+
if let site {
185+
builder(WordPressClient(site: site))
186+
} else {
187+
RestApiUpgradePrompt(localizedFeatureName: localizedFeatureName) {
188+
Task {
189+
await self.migrate()
190+
}
191+
}
192+
}
193+
}
194+
195+
@MainActor
196+
private func migrate() async {
197+
guard let url = try? blog.getUrlString() else {
198+
let error = NSLocalizedString("applicationPasswordMigration.error.siteUrlNotFound", value: "Cannot find the current site's url", comment: "Error message when the current site's url cannot be found")
199+
Notice(title: error).post()
200+
return
201+
}
202+
203+
do {
204+
// Get an application password for the given site.
205+
let authenticator = SelfHostedSiteAuthenticator(session: .shared)
206+
let success = try await authenticator.authentication(site: url, from: nil)
207+
208+
// Ensure the application password belongs to the current signed in user
209+
if let username = blog.username, success.userLogin != username {
210+
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")
211+
Notice(title: .localizedStringWithFormat(format, username)).post()
212+
return
213+
}
214+
215+
try blog.setApplicationToken(success.password)
216+
217+
// Modify the `site` variable to display the intended feature.
218+
self.site = try .init(baseUrl: ParsedUrl.parse(input: success.siteUrl), type: .selfHosted(username: success.userLogin, authToken: success.password))
219+
} catch let error as WordPressLoginClient.Error {
220+
if let message = error.errorMessage {
221+
Notice(title: message).post()
222+
}
223+
} catch {
224+
Notice(title: SharedStrings.Error.generic).post()
225+
}
226+
}
227+
}

0 commit comments

Comments
 (0)