Skip to content

Commit 45e3c2f

Browse files
authored
Merge pull request #7608 from woocommerce/issue/7597-ui-updates
Login: Support setting up Jetpack connection for logging in with site credentials
2 parents a585234 + f40549c commit 45e3c2f

22 files changed

+638
-16
lines changed

Networking/Networking/Network/WordPressOrgNetwork.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public final class WordPressOrgNetwork: Network {
2525
return sessionManager
2626
}()
2727

28-
public init(authenticator: Authenticator? = nil, userAgent: String? = nil) {
28+
public init(authenticator: Authenticator? = nil, userAgent: String = UserAgent.defaultUserAgent) {
2929
self.authenticator = authenticator
3030
self.userAgent = userAgent
3131
}

Podfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ target 'WooCommerce' do
5252
pod 'Gridicons', '~> 1.2.0'
5353

5454
# To allow pod to pick up beta versions use -beta. E.g., 1.1.7-beta.1
55-
pod 'WordPressAuthenticator', '~> 2.4.0'
56-
# pod 'WordPressAuthenticator', :git => 'https://github.com/wordpress-mobile/WordPressAuthenticator-iOS.git', :commit => ''
55+
pod 'WordPressAuthenticator', '~> 3.1.0-beta.1'
56+
# pod 'WordPressAuthenticator', :git => 'https://github.com/wordpress-mobile/WordPressAuthenticator-iOS.git', :commit => ''
5757
# pod 'WordPressAuthenticator', :git => 'https://github.com/wordpress-mobile/WordPressAuthenticator-iOS.git', :branch => ''
5858
# pod 'WordPressAuthenticator', :path => '../WordPressAuthenticator-iOS'
5959

Podfile.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ PODS:
4242
- WordPress-Aztec-iOS (1.11.0)
4343
- WordPress-Editor-iOS (1.11.0):
4444
- WordPress-Aztec-iOS (= 1.11.0)
45-
- WordPressAuthenticator (2.4.0):
45+
- WordPressAuthenticator (3.1.0-beta.1):
4646
- Alamofire (~> 4.8)
4747
- CocoaLumberjack (~> 3.5)
4848
- GoogleSignIn (~> 6.0.1)
@@ -92,7 +92,7 @@ DEPENDENCIES:
9292
- Sourcery (~> 1.0.3)
9393
- StripeTerminal (~> 2.7)
9494
- WordPress-Editor-iOS (~> 1.11.0)
95-
- WordPressAuthenticator (~> 2.4.0)
95+
- WordPressAuthenticator (~> 3.1.0-beta.1)
9696
- WordPressKit (~> 4.49.0)
9797
- WordPressShared (~> 1.15)
9898
- WordPressUI (~> 1.12.5)
@@ -163,7 +163,7 @@ SPEC CHECKSUMS:
163163
UIDeviceIdentifier: af4e11e25a2ea670078e2bd677bb0e8144f9f063
164164
WordPress-Aztec-iOS: 050b34d4c3adfb7c60363849049b13d60683b348
165165
WordPress-Editor-iOS: 304098424f1051cb271546c99f906aac296b1b81
166-
WordPressAuthenticator: afc1b7ec564f2b8e71c3d2ab8afa29df70ed6d03
166+
WordPressAuthenticator: 7adf1ab6dea0b1e01c519feb5cd2fc160f47798e
167167
WordPressKit: 96deb6ba37ea5eaec4ddcaa53eca04d653246152
168168
WordPressShared: 5477f179c7fe03b5d574f91adda66f67d131827e
169169
WordPressUI: c5be816f6c7b3392224ac21de9e521e89fa108ac
@@ -179,6 +179,6 @@ SPEC CHECKSUMS:
179179
ZendeskSupportProvidersSDK: 2bdf8544f7cd0fd4c002546f5704b813845beb2a
180180
ZendeskSupportSDK: 3a8e508ab1d9dd22dc038df6c694466414e037ba
181181

182-
PODFILE CHECKSUM: a9915adfd885798e630904e5ae74f77f8555ccfe
182+
PODFILE CHECKSUM: 777d68ac2ad78e410f8930be0fecb3b38338c2ae
183183

184184
COCOAPODS: 1.11.2

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
10.4
44
-----
55
- [*] Help center: Added help center web page with FAQs for "Pick a WooCommerce Store" screen. [https://github.com/woocommerce/woocommerce-ios/pull/7641]
6+
- [*] Login: Users can now set up the Jetpack connection between a self-hosted site and their WP.com account. [https://github.com/woocommerce/woocommerce-ios/pull/7608]
67

78
10.3
89
-----

WooCommerce/Classes/Authentication/AuthenticationManager.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,18 @@ class AuthenticationManager: Authentication {
189189
/// and returns an error view controller if not.
190190
func errorViewController(for siteURL: String,
191191
with matcher: ULAccountMatcher,
192+
credentials: AuthenticatorCredentials? = nil,
192193
navigationController: UINavigationController,
193194
onStorePickerDismiss: @escaping () -> Void) -> UIViewController? {
194195

195196
/// Account mismatched case
196197
guard matcher.match(originalURL: siteURL) else {
198+
/// Account mismatch experiment iteration 1: show jetpack connection error
199+
/// if the error happens during site credential login.
200+
if let credentials = credentials?.wporg {
201+
DDLogWarn("⚠️ Present Jetpack connection error for site: \(String(describing: siteURL))")
202+
return jetpackConnectionUI(for: siteURL, with: credentials, in: navigationController)
203+
}
197204
DDLogWarn("⚠️ Present account mismatch error for site: \(String(describing: siteURL))")
198205
return accountMismatchUI(for: siteURL, with: matcher)
199206
}
@@ -350,7 +357,11 @@ extension AuthenticationManager: WordPressAuthenticatorDelegate {
350357
let matcher = ULAccountMatcher(storageManager: storageManager)
351358
matcher.refreshStoredSites()
352359

353-
if let vc = errorViewController(for: siteURL, with: matcher, navigationController: navigationController, onStorePickerDismiss: onDismiss) {
360+
if let vc = errorViewController(for: siteURL,
361+
with: matcher,
362+
credentials: credentials,
363+
navigationController: navigationController,
364+
onStorePickerDismiss: onDismiss) {
354365
loggedOutAppSettings?.setErrorLoginSiteAddress(siteURL)
355366
navigationController.show(vc, sender: nil)
356367
} else {
@@ -607,8 +618,27 @@ private extension AuthenticationManager {
607618
return ULErrorViewController(viewModel: viewModel)
608619
}
609620

621+
/// The error screen to be displayed when the user tries to enter as site
622+
/// whose Jetpack is not connected to their WP.com account.
623+
/// This screen is currently displayed when user logged in with site credentials.
624+
///
625+
func jetpackConnectionUI(for siteURL: String,
626+
with credentials: WordPressOrgCredentials,
627+
in navigationController: UINavigationController) -> UIViewController {
628+
let viewModel = JetpackConnectionErrorViewModel(siteURL: siteURL, credentials: credentials, onJetpackSetupCompletion: { email in
629+
return WordPressAuthenticator.showVerifyEmailForWPCom(
630+
from: navigationController,
631+
xmlrpc: credentials.xmlrpc,
632+
connectedEmail: email,
633+
siteURL: siteURL
634+
)
635+
})
636+
return ULErrorViewController(viewModel: viewModel)
637+
}
638+
610639
/// The error screen to be displayed when the user tries to enter a site
611640
/// whose Jetpack is not associated with their account.
641+
/// This screen is currently displayed when user logged in with a WP.com account.
612642
///
613643
func accountMismatchUI(for siteURL: String, with matcher: ULAccountMatcher) -> UIViewController {
614644
let viewModel = WrongAccountErrorViewModel(siteURL: siteURL, showsConnectedStores: matcher.hasConnectedStores)
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import Combine
2+
import Foundation
3+
import Yosemite
4+
import WordPressAuthenticator
5+
import class Networking.WordPressOrgNetwork
6+
7+
final class JetpackConnectionErrorViewModel: ULErrorViewModel {
8+
private let siteURL: String
9+
private var jetpackConnectionURL: URL?
10+
private let stores: StoresManager
11+
private let isPrimaryButtonLoadingSubject = CurrentValueSubject<Bool, Never>(false)
12+
private let jetpackSetupCompletionHandler: (String) -> Void
13+
14+
init(siteURL: String,
15+
credentials: WordPressOrgCredentials,
16+
stores: StoresManager = ServiceLocator.stores,
17+
onJetpackSetupCompletion: @escaping (String) -> Void) {
18+
self.siteURL = siteURL
19+
self.stores = stores
20+
self.jetpackSetupCompletionHandler = onJetpackSetupCompletion
21+
authenticate(with: credentials)
22+
fetchJetpackConnectionURL()
23+
}
24+
25+
// MARK: - Data and configuration
26+
27+
let image: UIImage = .productErrorImage
28+
29+
var text: NSAttributedString {
30+
let font: UIFont = .body
31+
let boldFont: UIFont = font.bold
32+
33+
let boldSiteAddress = NSAttributedString(string: siteURL.trimHTTPScheme(),
34+
attributes: [.font: boldFont])
35+
let attributedString = NSMutableAttributedString(string: Localization.noJetpackEmail)
36+
attributedString.replaceFirstOccurrence(of: "%@", with: boldSiteAddress)
37+
38+
return attributedString
39+
}
40+
41+
let isAuxiliaryButtonHidden = true
42+
43+
let auxiliaryButtonTitle = ""
44+
45+
let primaryButtonTitle = Localization.primaryButtonTitle
46+
47+
var isPrimaryButtonLoading: AnyPublisher<Bool, Never> {
48+
isPrimaryButtonLoadingSubject.eraseToAnyPublisher()
49+
}
50+
51+
let secondaryButtonTitle = Localization.secondaryButtonTitle
52+
53+
func viewDidLoad(_ viewController: UIViewController?) {
54+
// TODO: Tracks?
55+
}
56+
57+
func didTapPrimaryButton(in viewController: UIViewController?) {
58+
showJetpackConnectionWebView(from: viewController)
59+
}
60+
61+
func didTapSecondaryButton(in viewController: UIViewController?) {
62+
viewController?.navigationController?.popToRootViewController(animated: true)
63+
}
64+
65+
func didTapAuxiliaryButton(in viewController: UIViewController?) {
66+
// no-op
67+
}
68+
}
69+
70+
// MARK: - Private helpers
71+
private extension JetpackConnectionErrorViewModel {
72+
/// Presents a web view pointing to the Jetpack connection URL.
73+
///
74+
func showJetpackConnectionWebView(from viewController: UIViewController?) {
75+
guard let viewController = viewController else {
76+
return
77+
}
78+
guard let url = jetpackConnectionURL else {
79+
DDLogWarn("⚠️ No Jetpack connection URL found")
80+
return
81+
}
82+
let viewModel = JetpackConnectionWebViewModel(initialURL: url, siteURL: siteURL, completion: { [weak self] in
83+
self?.fetchJetpackUser(in: viewController)
84+
})
85+
let pluginViewController = AuthenticatedWebViewController(viewModel: viewModel)
86+
viewController.navigationController?.show(pluginViewController, sender: nil)
87+
}
88+
89+
/// Prepares `JetpackConnectionStore` to authenticate subsequent requests to WP.org API.
90+
///
91+
func authenticate(with credentials: WordPressOrgCredentials) {
92+
guard let authenticator = credentials.makeCookieNonceAuthenticator() else {
93+
return
94+
}
95+
let network = WordPressOrgNetwork(authenticator: authenticator)
96+
let action = JetpackConnectionAction.authenticate(siteURL: siteURL, network: network)
97+
stores.dispatch(action)
98+
}
99+
100+
/// Fetches the URL for handling Jetpack connection in a web view
101+
///
102+
func fetchJetpackConnectionURL() {
103+
isPrimaryButtonLoadingSubject.send(true)
104+
let action = JetpackConnectionAction.fetchJetpackConnectionURL { [weak self] result in
105+
guard let self = self else { return }
106+
self.isPrimaryButtonLoadingSubject.send(false)
107+
switch result {
108+
case .success(let url):
109+
self.jetpackConnectionURL = url
110+
case .failure(let error):
111+
DDLogWarn("⚠️ Error fetching Jetpack connection URL: \(error)")
112+
}
113+
}
114+
stores.dispatch(action)
115+
}
116+
117+
/// Gets the connected WP.com email address if possible, or show error otherwise.
118+
///
119+
func fetchJetpackUser(in viewController: UIViewController) {
120+
showInProgressView(in: viewController)
121+
let action = JetpackConnectionAction.fetchJetpackUser { [weak self] result in
122+
guard let self = self else { return }
123+
// dismisses the in-progress view
124+
viewController.navigationController?.dismiss(animated: true)
125+
126+
switch result {
127+
case .success(let user):
128+
guard let emailAddress = user.wpcomUser?.email else {
129+
DDLogWarn("⚠️ Cannot find connected WPcom user")
130+
return self.showSetupErrorNotice(in: viewController)
131+
}
132+
self.jetpackSetupCompletionHandler(emailAddress)
133+
case .failure(let error):
134+
DDLogWarn("⚠️ Error fetching Jetpack user: \(error)")
135+
self.showSetupErrorNotice(in: viewController)
136+
}
137+
}
138+
stores.dispatch(action)
139+
}
140+
141+
func showInProgressView(in viewController: UIViewController) {
142+
let viewProperties = InProgressViewProperties(title: Localization.inProgressMessage, message: "")
143+
let inProgressViewController = InProgressViewController(viewProperties: viewProperties)
144+
inProgressViewController.modalPresentationStyle = .overCurrentContext
145+
146+
viewController.navigationController?.present(inProgressViewController, animated: true, completion: nil)
147+
}
148+
149+
func showSetupErrorNotice(in viewController: UIViewController) {
150+
let message = Localization.setupErrorMessage
151+
let notice = Notice(title: message, feedbackType: .error)
152+
let noticePresenter = DefaultNoticePresenter()
153+
noticePresenter.presentingViewController = viewController
154+
noticePresenter.enqueue(notice: notice)
155+
}
156+
}
157+
158+
private extension JetpackConnectionErrorViewModel {
159+
enum Localization {
160+
static let noJetpackEmail = NSLocalizedString(
161+
"It looks like your account is not connected to %@'s Jetpack",
162+
comment: "Message explaining that the entered site credentials belong to an account that is not connected to the site's Jetpack. "
163+
+ "Reads like 'It looks like your account is not connected to awebsite.com's Jetpack")
164+
165+
static let primaryButtonTitle = NSLocalizedString(
166+
"Connect Jetpack to your account",
167+
comment: "Button linking to web view for setting up Jetpack connection. " +
168+
"Presented when logging in with store credentials of an account not connected to the site's Jetpack")
169+
170+
static let secondaryButtonTitle = NSLocalizedString(
171+
"Log In With Another Account",
172+
comment: "Action button that will restart the login flow." +
173+
"Presented when logging in with store credentials of an account not connected to the site's Jetpack"
174+
)
175+
176+
static let inProgressMessage = NSLocalizedString(
177+
"Verifying Jetpack connection...",
178+
comment: "Message displayed when checking whether Jetpack has been connected successfully"
179+
)
180+
181+
static let setupErrorMessage = NSLocalizedString(
182+
"Cannot verify your Jetpack connection. Please try again.",
183+
comment: "Error message displayed when failed to check for Jetpack connection."
184+
)
185+
}
186+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import Foundation
2+
import WebKit
3+
4+
/// View model used for the web view controller to setup Jetpack connection during the login flow.
5+
///
6+
final class JetpackConnectionWebViewModel: AuthenticatedWebViewModel {
7+
let title = Localization.title
8+
9+
let initialURL: URL?
10+
let siteURL: String
11+
let completionHandler: () -> Void
12+
13+
init(initialURL: URL, siteURL: String, completion: @escaping () -> Void) {
14+
self.initialURL = initialURL
15+
self.siteURL = siteURL
16+
self.completionHandler = completion
17+
}
18+
19+
func handleDismissal() {
20+
// TODO: tracks?
21+
}
22+
23+
func handleRedirect(for url: URL?) {
24+
// No-op
25+
}
26+
27+
func decidePolicy(for navigationURL: URL) async -> WKNavigationActionPolicy {
28+
let url = navigationURL.absoluteString
29+
switch url {
30+
// When the web view navigates to the site address or Jetpack plans page,
31+
// we can assume that the setup has completed.
32+
case let url where url.hasPrefix(siteURL) || url.hasPrefix(Constants.plansPage):
33+
await MainActor.run { [weak self] in
34+
self?.handleSetupCompletion()
35+
}
36+
return .cancel
37+
default:
38+
return .allow
39+
}
40+
}
41+
42+
private func handleSetupCompletion() {
43+
// TODO: tracks?
44+
completionHandler()
45+
}
46+
}
47+
48+
private extension JetpackConnectionWebViewModel {
49+
enum Constants {
50+
static let plansPage = "https://wordpress.com/jetpack/connect/plans"
51+
}
52+
53+
enum Localization {
54+
static let title = NSLocalizedString("Connect Jetpack", comment: "Title of the Jetpack connection web view in the login flow")
55+
}
56+
}

0 commit comments

Comments
 (0)