Skip to content

Commit 10a431f

Browse files
authored
Merge pull request #7773 from woocommerce/issue/7766-suggestion-connect-jectpack-wpcom
Login: Show instruction to connect to a WP.com site from the account mismatch error screen
2 parents 401163e + cd3c46d commit 10a431f

10 files changed

+122
-137
lines changed

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
- [**] Products tab: products search now has an option to search products by SKU. Stores with WC version 6.6+ support partial SKU search, otherwise the product(s) with the exact SKU match is returned. [https://github.com/woocommerce/woocommerce-ios/pull/7781]
77
- [*] Fixed a rare crash when selecting a store in the store picker. [https://github.com/woocommerce/woocommerce-ios/pull/7765]
8+
- [*] Show suggestion for logging in to a WP.com site with a mismatched WP.com account. [https://github.com/woocommerce/woocommerce-ios/pull/7773]
89
- [*] Help center: Added help center web page with FAQs for "Not a WooCommerce site" and "Wrong WordPress.com account" error screens. [https://github.com/woocommerce/woocommerce-ios/pull/7767, https://github.com/woocommerce/woocommerce-ios/pull/7769]
910

1011
10.5

WooCommerce/Classes/Authentication/Navigation Exceptions/JetpackConnectionWebViewModel.swift

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,28 +27,38 @@ final class JetpackConnectionWebViewModel: AuthenticatedWebViewModel {
2727
}
2828

2929
func handleRedirect(for url: URL?) {
30-
// No-op
30+
guard let path = url?.absoluteString else {
31+
return
32+
}
33+
handleCompletionIfPossible(path)
3134
}
3235

3336
func decidePolicy(for navigationURL: URL) async -> WKNavigationActionPolicy {
3437
let url = navigationURL.absoluteString
35-
switch url {
36-
// When the web view navigates to the site address or Jetpack plans page,
37-
// we can assume that the setup has completed.
38-
case let url where url.hasPrefix(siteURL) || url.hasPrefix(Constants.plansPage):
39-
await MainActor.run { [weak self] in
40-
self?.handleSetupCompletion()
41-
}
38+
if handleCompletionIfPossible(url) {
4239
return .cancel
43-
default:
44-
return .allow
4540
}
41+
return .allow
4642
}
4743

4844
private func handleSetupCompletion() {
4945
analytics.track(.loginJetpackConnectCompleted)
5046
completionHandler()
5147
}
48+
49+
@discardableResult
50+
func handleCompletionIfPossible(_ url: String) -> Bool {
51+
// When the web view navigates to the site address or Jetpack plans page,
52+
// we can assume that the setup has completed.
53+
if url.hasPrefix(siteURL) || url.hasPrefix(Constants.plansPage) {
54+
// Running on the main thread is necessary if this method is triggered from `decidePolicy`.
55+
DispatchQueue.main.async { [weak self] in
56+
self?.handleSetupCompletion()
57+
}
58+
return true
59+
}
60+
return false
61+
}
5262
}
5363

5464
private extension JetpackConnectionWebViewModel {

WooCommerce/Classes/Authentication/Navigation Exceptions/ULAccountMismatchViewController.swift

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ final class ULAccountMismatchViewController: UIViewController {
2727
@IBOutlet private weak var logOutButton: UIButton!
2828
@IBOutlet private weak var primaryButton: NUXButton!
2929
@IBOutlet private weak var secondaryButton: NUXButton!
30-
@IBOutlet private weak var activityIndicator: UIActivityIndicatorView!
3130

3231
@IBOutlet private weak var imageView: UIImageView!
3332
@IBOutlet private weak var errorMessage: UILabel!
@@ -65,7 +64,6 @@ final class ULAccountMismatchViewController: UIViewController {
6564

6665
configurePrimaryButton()
6766
configureSecondaryButon()
68-
configureActivityIndicator()
6967

7068
setUnifiedMargins(forWidth: view.frame.width)
7169

@@ -162,12 +160,6 @@ private extension ULAccountMismatchViewController {
162160
self?.primaryButton.showActivityIndicator(isLoading)
163161
}
164162
.store(in: &subscriptions)
165-
166-
viewModel.isPrimaryButtonHidden
167-
.sink { [weak self] isHidden in
168-
self?.primaryButton.isHidden = isHidden
169-
}
170-
.store(in: &subscriptions)
171163
}
172164

173165
func configureSecondaryButon() {
@@ -179,20 +171,6 @@ private extension ULAccountMismatchViewController {
179171
}
180172
}
181173

182-
func configureActivityIndicator() {
183-
activityIndicator.hidesWhenStopped = true
184-
185-
viewModel.isShowingActivityIndicator
186-
.sink { [weak self] showing in
187-
if showing {
188-
self?.activityIndicator.startAnimating()
189-
} else {
190-
self?.activityIndicator.stopAnimating()
191-
}
192-
}
193-
.store(in: &subscriptions)
194-
}
195-
196174
/// This logic is lifted from WPAuthenticator's LoginPrologueViewController
197175
/// This View Controller will be provided to WPAuthenticator. WPAuthenticator
198176
/// will insert it into its own navigation stack, where it is applying a similar logic
@@ -282,10 +260,6 @@ extension ULAccountMismatchViewController {
282260
return secondaryButton
283261
}
284262

285-
func getActivityIndicator() -> UIActivityIndicatorView {
286-
return activityIndicator
287-
}
288-
289263
func getUserNameLabel() -> UILabel {
290264
return userNameLabel
291265
}

WooCommerce/Classes/Authentication/Navigation Exceptions/ULAccountMismatchViewController.xib

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
<objects>
1212
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="ULAccountMismatchViewController" customModule="WooCommerce" customModuleProvider="target">
1313
<connections>
14-
<outlet property="activityIndicator" destination="GcM-pl-Wag" id="SyY-Mz-h3S"/>
1514
<outlet property="buttonViewLeadingConstraint" destination="r9Z-y5-j3W" id="bWb-vV-0U2"/>
1615
<outlet property="buttonViewTrailingConstraint" destination="3Fe-tr-bmb" id="DjF-95-KFx"/>
1716
<outlet property="errorMessage" destination="a2d-le-aKc" id="YgT-Ex-Gwh"/>
@@ -35,17 +34,13 @@
3534
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
3635
<subviews>
3736
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="10Z-y1-ZQ6" userLabel="Action Background View">
38-
<rect key="frame" x="0.0" y="662" width="414" height="200"/>
37+
<rect key="frame" x="0.0" y="702" width="414" height="160"/>
3938
<subviews>
4039
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="mhI-fa-Yzw">
41-
<rect key="frame" x="20" y="20" width="374" height="160"/>
40+
<rect key="frame" x="20" y="20" width="374" height="120"/>
4241
<subviews>
43-
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" style="medium" id="GcM-pl-Wag">
44-
<rect key="frame" x="0.0" y="0.0" width="374" height="20"/>
45-
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
46-
</activityIndicatorView>
4742
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ZHa-is-GJK" userLabel="secondary action button" customClass="NUXButton" customModule="WordPressAuthenticator">
48-
<rect key="frame" x="0.0" y="40" width="374" height="50"/>
43+
<rect key="frame" x="0.0" y="0.0" width="374" height="50"/>
4944
<constraints>
5045
<constraint firstAttribute="height" constant="50" id="Gcd-Af-CS9"/>
5146
</constraints>
@@ -55,7 +50,7 @@
5550
</state>
5651
</button>
5752
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="0ZR-ma-6pq" userLabel="secondary action button" customClass="NUXButton" customModule="WordPressAuthenticator">
58-
<rect key="frame" x="0.0" y="110" width="374" height="50"/>
53+
<rect key="frame" x="0.0" y="70" width="374" height="50"/>
5954
<constraints>
6055
<constraint firstAttribute="height" constant="50" id="BTA-bw-h4s"/>
6156
</constraints>
@@ -75,7 +70,7 @@
7570
</constraints>
7671
</view>
7772
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="jIt-xb-rrN">
78-
<rect key="frame" x="34" y="176" width="346" height="486"/>
73+
<rect key="frame" x="34" y="192" width="346" height="486"/>
7974
<subviews>
8075
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Np4-PU-IRZ" customClass="CircularImageView" customModule="WooCommerce" customModuleProvider="target">
8176
<rect key="frame" x="143" y="0.0" width="60" height="60"/>

WooCommerce/Classes/Authentication/Navigation Exceptions/ULAccountMismatchViewModel.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ protocol ULAccountMismatchViewModel {
2828
/// Provides a title for a primary action button
2929
var primaryButtonTitle: String { get }
3030

31-
/// Provides the visibility of the primary button
32-
var isPrimaryButtonHidden: AnyPublisher<Bool, Never> { get }
33-
3431
/// Provides the loading state of the primary button
3532
var isPrimaryButtonLoading: AnyPublisher<Bool, Never> { get }
3633

@@ -40,9 +37,6 @@ protocol ULAccountMismatchViewModel {
4037
/// Provides the visibility of the secondary button
4138
var isSecondaryButtonHidden: Bool { get }
4239

43-
/// Provides the visibility of the activity indicator
44-
var isShowingActivityIndicator: AnyPublisher<Bool, Never> { get }
45-
4640
/// Provides the title for the logout button
4741
var logOutButtonTitle: String { get }
4842

WooCommerce/Classes/Authentication/Navigation Exceptions/WrongAccountErrorViewModel.swift

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,10 @@ final class WrongAccountErrorViewModel: ULAccountMismatchViewModel {
2424
private var siteUsername: String = ""
2525
private var jetpackConnectionURL: URL?
2626

27-
private let primaryButtonHiddenSubject = CurrentValueSubject<Bool, Never>(true)
28-
private let primaryButtonLoadingSubject = CurrentValueSubject<Bool, Never>(false)
29-
private let activityIndicatorLoadingSubject = CurrentValueSubject<Bool, Never>(false)
27+
@Published private var isSelfHostedSite = false
28+
@Published private var primaryButtonLoading = false
3029

31-
private var primaryButtonSubscription: AnyCancellable?
30+
private var siteInfoSubscription: AnyCancellable?
3231

3332
init(siteURL: String?,
3433
showsConnectedStores: Bool,
@@ -96,19 +95,12 @@ final class WrongAccountErrorViewModel: ULAccountMismatchViewModel {
9695

9796
let secondaryButtonTitle = Localization.secondaryButtonTitle
9897

99-
var isPrimaryButtonHidden: AnyPublisher<Bool, Never> {
100-
primaryButtonHiddenSubject.eraseToAnyPublisher()
101-
}
102-
10398
var isPrimaryButtonLoading: AnyPublisher<Bool, Never> {
104-
primaryButtonLoadingSubject.eraseToAnyPublisher()
99+
$primaryButtonLoading.eraseToAnyPublisher()
105100
}
106101

107102
var isSecondaryButtonHidden: Bool { !showsConnectedStores }
108103

109-
var isShowingActivityIndicator: AnyPublisher<Bool, Never> {
110-
activityIndicatorLoadingSubject.eraseToAnyPublisher()
111-
}
112104

113105
// Configures `Help` button title
114106
var rightBarButtonItemTitle: String? {
@@ -117,19 +109,17 @@ final class WrongAccountErrorViewModel: ULAccountMismatchViewModel {
117109

118110
// MARK: - Actions
119111
func viewDidLoad(_ viewController: UIViewController?) {
120-
primaryButtonSubscription = primaryButtonHiddenSubject
112+
siteInfoSubscription = $isSelfHostedSite
121113
.dropFirst() // ignores first element
122-
.sink { [weak self] isHidden in
123-
// if the button is hidden, the site is not self-hosted.
124-
self?.analytics.track(event: .LoginJetpackConnection.jetpackConnectionErrorShown(selfHostedSite: !isHidden))
114+
.sink { [weak self] isSelfHosted in
115+
self?.analytics.track(event: .LoginJetpackConnection.jetpackConnectionErrorShown(selfHostedSite: isSelfHosted))
125116
}
126117

127118
// Fetches site info if we're not sure whether the site is self-hosted.
128119
if siteXMLRPC.isEmpty {
129120
fetchSiteInfo()
130121
} else {
131-
// Shows the Connect Jetpack button if the site is self-hosted
132-
primaryButtonHiddenSubject.send(false)
122+
isSelfHostedSite = true
133123
}
134124
}
135125

@@ -140,7 +130,10 @@ final class WrongAccountErrorViewModel: ULAccountMismatchViewModel {
140130
}
141131

142132
guard let url = jetpackConnectionURL else {
143-
return showSiteCredentialLoginAndJetpackConnection(from: viewController)
133+
if isSelfHostedSite {
134+
return showSiteCredentialLoginAndJetpackConnection(from: viewController)
135+
}
136+
return presentConnectToWPComSiteAlert(from: viewController)
144137
}
145138

146139
showJetpackConnectionWebView(url: url, from: viewController)
@@ -182,18 +175,14 @@ private extension WrongAccountErrorViewModel {
182175
/// If the site is self-hosted, make the Connect Jetpack button visible.
183176
///
184177
func fetchSiteInfo() {
185-
activityIndicatorLoadingSubject.send(true)
178+
primaryButtonLoading = true
186179
authenticatorType.fetchSiteInfo(for: siteURL) { [weak self] result in
187180
guard let self = self else { return }
188-
self.activityIndicatorLoadingSubject.send(false)
181+
self.primaryButtonLoading = false
189182

190183
switch result {
191184
case .success(let siteInfo):
192-
if siteInfo.isWPCom == false {
193-
self.primaryButtonHiddenSubject.send(false)
194-
} else {
195-
self.primaryButtonHiddenSubject.send(true)
196-
}
185+
self.isSelfHostedSite = !siteInfo.isWPCom
197186
case .failure(let error):
198187
DDLogWarn("⚠️ Error fetching site info: \(error)")
199188
}
@@ -214,10 +203,10 @@ private extension WrongAccountErrorViewModel {
214203
/// Fetches the URL for handling Jetpack connection in a web view
215204
///
216205
func fetchJetpackConnectionURL(onCompletion: ((URL) -> Void)? = nil) {
217-
primaryButtonLoadingSubject.send(true)
206+
primaryButtonLoading = true
218207
let action = JetpackConnectionAction.fetchJetpackConnectionURL { [weak self] result in
219208
guard let self = self else { return }
220-
self.primaryButtonLoadingSubject.send(false)
209+
self.primaryButtonLoading = false
221210
switch result {
222211
case .success(let url):
223212
onCompletion?(url)
@@ -244,6 +233,13 @@ private extension WrongAccountErrorViewModel {
244233
}
245234
}
246235

236+
func presentConnectToWPComSiteAlert(from viewController: UIViewController) {
237+
let fancyAlert = FancyAlertViewController.makeConnectAccountToWPComSiteAlert()
238+
fancyAlert.modalPresentationStyle = .custom
239+
fancyAlert.transitioningDelegate = AppDelegate.shared.tabBarController
240+
viewController.present(fancyAlert, animated: true)
241+
}
242+
247243
/// Presents a web view pointing to the Jetpack connection URL.
248244
///
249245
func showJetpackConnectionWebView(url: URL, from viewController: UIViewController) {
@@ -348,9 +344,9 @@ private extension WrongAccountErrorViewModel {
348344
comment: "Action button linking to a list of connected stores."
349345
+ "Presented when logging in with a store address that does not match the account entered")
350346

351-
static let primaryButtonTitle = NSLocalizedString("Connect Jetpack",
352-
comment: "Action button to handle Jetpack connection."
353-
+ "Presented when logging in with a self-hosted site that does not match the account entered")
347+
static let primaryButtonTitle = NSLocalizedString("Connect to the site",
348+
comment: "Action button to handle connecting the logged-in account to a given site."
349+
+ "Presented when logging in with a store address that does not match the account entered")
354350

355351
static let yourSite = NSLocalizedString("your site",
356352
comment: "Placeholder for site url, if the url is unknown."

WooCommerce/Classes/ViewRelated/Fancy Alerts/FancyAlertViewController+UnifiedLogin.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,20 @@ extension FancyAlertViewController {
3939
return controller
4040

4141
}
42+
43+
static func makeConnectAccountToWPComSiteAlert() -> FancyAlertViewController {
44+
let dismissButton = makeDismissButtonConfig()
45+
let config = FancyAlertViewController.Config(titleText: Localization.connectToWPComSite,
46+
bodyText: Localization.connectToWPComSiteDescription,
47+
headerImage: nil,
48+
dividerPosition: .top,
49+
defaultButton: dismissButton,
50+
cancelButton: nil,
51+
dismissAction: {})
52+
53+
let controller = FancyAlertViewController.controllerWithConfiguration(configuration: config)
54+
return controller
55+
}
4256
}
4357

4458

@@ -81,6 +95,18 @@ private extension FancyAlertViewController {
8195
"Need more help?",
8296
comment: "Title of button to learn more presented when users attempt to log in with an email address that does not match a WP.com account"
8397
)
98+
99+
static let connectToWPComSite = NSLocalizedString(
100+
"Connecting to a WordPress.com site",
101+
comment: "Title of alert for suggestion on how to connect to a WP.com site" +
102+
"Presented when a user logs in with an email that does not have access to a WP.com site"
103+
)
104+
105+
static let connectToWPComSiteDescription = NSLocalizedString(
106+
"Please contact the site owner for an invitation to the site as a shop manager or administrator to use the app.",
107+
comment: "Description of alert for suggestion on how to connect to a WP.com site" +
108+
"Presented when a user logs in with an email that does not have access to a WP.com site"
109+
)
84110
}
85111

86112
enum Strings {

WooCommerce/WooCommerceTests/Authentication/JetpackConnectionWebViewModelTests.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ final class JetpackConnectionWebViewModelTests: XCTestCase {
1818
let authorizePolicy = await viewModel.decidePolicy(for: authorizeURL)
1919
let finalUrl = try XCTUnwrap(URL(string: siteURL + "/wp-admin"))
2020
let completionPolicy = await viewModel.decidePolicy(for: finalUrl)
21+
waitUntil {
22+
completionTriggered == true
23+
}
2124

2225
// Then
2326
XCTAssertEqual(authorizePolicy, .allow)
2427
XCTAssertEqual(completionPolicy, .cancel)
25-
XCTAssertTrue(completionTriggered)
2628
}
2729

2830
func test_dismissal_is_tracked() throws {
@@ -46,13 +48,21 @@ final class JetpackConnectionWebViewModelTests: XCTestCase {
4648
let analyticsProvider = MockAnalyticsProvider()
4749
let analytics = WooAnalytics(analyticsProvider: analyticsProvider)
4850

51+
var completionTriggered = false
52+
let completionHandler: () -> Void = {
53+
completionTriggered = true
54+
}
55+
4956
let siteURL = "https://test.com"
5057
let initialURL = try XCTUnwrap(URL(string: "https://jetpack.wordpress.com/jetpack.authorize/1/"))
51-
let viewModel = JetpackConnectionWebViewModel(initialURL: initialURL, siteURL: siteURL, analytics: analytics, completion: {})
58+
let viewModel = JetpackConnectionWebViewModel(initialURL: initialURL, siteURL: siteURL, analytics: analytics, completion: completionHandler)
5259

5360
// When
5461
let finalUrl = try XCTUnwrap(URL(string: siteURL + "/wp-admin"))
5562
_ = await viewModel.decidePolicy(for: finalUrl)
63+
waitUntil {
64+
completionTriggered == true
65+
}
5666

5767
// Then
5868
XCTAssertNotNil(analyticsProvider.receivedEvents.first(where: { $0 == "login_jetpack_connect_completed" }))

0 commit comments

Comments
 (0)