Skip to content

Commit f9d544d

Browse files
authored
Merge pull request #8152 from woocommerce/issue/8075-native-jetpack-install
Login: Native Jetpack setup UI
2 parents 1099941 + 8a83da8 commit f9d544d

31 files changed

+1962
-50
lines changed

WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1653,14 +1653,22 @@ extension WooAnalyticsEvent {
16531653
enum Key: String {
16541654
case hasWordPress = "has_wordpress"
16551655
case isWPCom = "is_wpcom"
1656-
case hasValidJetpack = "has_valid_jetpack"
1656+
case isJetpackInstalled = "is_jetpack_installed"
1657+
case isJetpackActive = "is_jetpack_active"
1658+
case isJetpackConnected = "is_jetpack_connected"
16571659
}
16581660

16591661
/// Tracks when the result for site discovery is returned
1660-
static func siteDiscovery(hasWordPress: Bool, isWPCom: Bool, hasValidJetpack: Bool) -> WooAnalyticsEvent {
1662+
static func siteDiscovery(hasWordPress: Bool,
1663+
isWPCom: Bool,
1664+
isJetpackInstalled: Bool,
1665+
isJetpackActive: Bool,
1666+
isJetpackConnected: Bool) -> WooAnalyticsEvent {
16611667
WooAnalyticsEvent(statName: .sitePickerSiteDiscovery, properties: [Key.hasWordPress.rawValue: hasWordPress,
16621668
Key.isWPCom.rawValue: isWPCom,
1663-
Key.hasValidJetpack.rawValue: hasValidJetpack])
1669+
Key.isJetpackInstalled.rawValue: isJetpackInstalled,
1670+
Key.isJetpackActive.rawValue: isJetpackActive,
1671+
Key.isJetpackConnected.rawValue: isJetpackConnected])
16641672
}
16651673

16661674
/// Tracks when the user taps the New To WooCommerce button

WooCommerce/Classes/Analytics/WooAnalyticsStat.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,43 @@ public enum WooAnalyticsStat: String {
8383
case whatIsWPComOnInvalidEmailScreenTapped = "what_is_wordpress_com_on_invalid_email_screen"
8484
case createAccountOnInvalidEmailScreenTapped = "create_account_on_invalid_email_screen"
8585

86+
// MARK: Site credentials
87+
//
88+
case loginJetpackSiteCredentialScreenViewed = "login_jetpack_site_credential_screen_viewed"
89+
case loginJetpackSiteCredentialScreenDismissed = "login_jetpack_site_credential_screen_dismissed"
90+
case loginJetpackSiteCredentialInstallTapped = "login_jetpack_site_credential_install_button_tapped"
91+
case loginJetpackSiteCredentialResetPasswordTapped = "login_jetpack_site_credential_reset_password_button_tapped"
92+
case loginJetpackSiteCredentialDidShowErrorAlert = "login_jetpack_site_credential_did_show_error_alert"
93+
case loginJetpackSiteCredentialDidFinishLogin = "login_jetpack_site_credential_did_finish_login"
94+
95+
// MARK: Install/Setup Jetpack (`LoginJetpackSetupView`)
96+
//
97+
case loginJetpackSetupScreenViewed = "login_jetpack_setup_screen_viewed"
98+
case loginJetpackSetupScreenDismissed = "login_jetpack_setup_screen_dismissed"
99+
100+
case loginJetpackSetupScreenInstallSuccessful = "login_jetpack_setup_install_successful"
101+
case loginJetpackSetupScreenInstallFailed = "login_jetpack_setup_install_failed"
102+
103+
case loginJetpackSetupActivationSuccessful = "login_jetpack_setup_activation_successful"
104+
case loginJetpackSetupActivationFailed = "login_jetpack_setup_activation_failed"
105+
106+
case loginJetpackSetupFetchJetpackConnectionURLSuccessful = "login_jetpack_setup_fetch_jetpack_connection_url_successful"
107+
case loginJetpackSetupFetchJetpackConnectionURLFailed = "login_jetpack_setup_fetch_jetpack_connection_url_failed"
108+
109+
case loginJetpackSetupCannotFindWPCOMUser = "login_jetpack_setup_cannot_find_WPCOM_user"
110+
case loginJetpackSetupAllStepsMarkedDone = "login_jetpack_setup_all_steps_marked_done"
111+
case loginJetpackSetupErrorCheckingJetpackConnection = "login_jetpack_setup_error_checking_jetpack_connection"
112+
113+
case loginJetpackSetupGoToStoreTapped = "login_jetpack_setup_go_to_store_button_tapped"
114+
115+
case loginJetpackSetupAuthorizedUsingDifferentWPCOMAccount = "login_jetpack_setup_authorized_using_different_wpcom_account"
116+
117+
// MARK: No matched site alert
118+
//
119+
case loginJetpackNoMatchedSiteErrorViewed = "login_jetpack_no_matched_site_error_viewed"
120+
case loginJetpackNoMatchedSiteErrorTryAgainButtonTapped = "login_jetpack_no_matched_site_error_try_again_button_tapped"
121+
case loginJetpackNoMatchedSiteErrorContactSupportButtonTapped = "login_jetpack_no_matched_site_error_contact_support_button_tapped"
122+
86123
// MARK: Dashboard View Events
87124
//
88125
case dashboardSelected = "main_tab_dashboard_selected"

WooCommerce/Classes/Authentication/AuthenticationManager.swift

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,14 @@ class AuthenticationManager: Authentication {
4343

4444
private let featureFlagService: FeatureFlagService
4545

46+
private let analytics: Analytics
47+
4648
init(storageManager: StorageManagerType = ServiceLocator.storageManager,
47-
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) {
49+
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService,
50+
analytics: Analytics = ServiceLocator.analytics) {
4851
self.storageManager = storageManager
4952
self.featureFlagService = featureFlagService
53+
self.analytics = analytics
5054
}
5155

5256
/// Initializes the WordPress Authenticator.
@@ -181,7 +185,7 @@ class AuthenticationManager: Authentication {
181185
// Resets Apple ID at the beginning of the authentication.
182186
self.appleUserID = nil
183187

184-
ServiceLocator.analytics.track(.loginPrologueContinueTapped)
188+
self.analytics.track(.loginPrologueContinueTapped)
185189
})
186190
guard let loginVC = loginUI else {
187191
fatalError("Cannot instantiate login UI from WordPressAuthenticator")
@@ -347,9 +351,11 @@ extension AuthenticationManager: WordPressAuthenticatorDelegate {
347351
/// Data flow following ZwYqDGHdenvYZoPHXZ1SOf-fi
348352
///
349353
func troubleshootSite(_ siteInfo: WordPressComSiteInfo?, in navigationController: UINavigationController?) {
350-
ServiceLocator.analytics.track(event: .SitePicker.siteDiscovery(hasWordPress: siteInfo?.isWP ?? false,
351-
isWPCom: siteInfo?.isWPCom ?? false,
352-
hasValidJetpack: siteInfo?.isJetpackConnected ?? false))
354+
analytics.track(event: .SitePicker.siteDiscovery(hasWordPress: siteInfo?.isWP ?? false,
355+
isWPCom: siteInfo?.isWPCom ?? false,
356+
isJetpackInstalled: siteInfo?.hasJetpack ?? false,
357+
isJetpackActive: siteInfo?.isJetpackActive ?? false,
358+
isJetpackConnected: siteInfo?.isJetpackConnected ?? false))
353359

354360
guard let site = siteInfo, let navigationController = navigationController else {
355361
navigationController?.show(noWPUI, sender: nil)
@@ -509,7 +515,7 @@ extension AuthenticationManager: WordPressAuthenticatorDelegate {
509515
DDLogWarn("⚠️ Could not convert WPAnalyticsStat with value: \(event.rawValue)")
510516
return
511517
}
512-
ServiceLocator.analytics.track(wooEvent)
518+
analytics.track(wooEvent)
513519
}
514520

515521
/// Tracks a given Analytics Event, with the specified properties.
@@ -519,7 +525,7 @@ extension AuthenticationManager: WordPressAuthenticatorDelegate {
519525
DDLogWarn("⚠️ Could not convert WPAnalyticsStat with value: \(event.rawValue)")
520526
return
521527
}
522-
ServiceLocator.analytics.track(wooEvent, withProperties: properties)
528+
analytics.track(wooEvent, withProperties: properties)
523529
}
524530

525531
/// Tracks a given Analytics Event, with the specified error.
@@ -529,12 +535,12 @@ extension AuthenticationManager: WordPressAuthenticatorDelegate {
529535
DDLogWarn("⚠️ Could not convert WPAnalyticsStat with value: \(event.rawValue)")
530536
return
531537
}
532-
ServiceLocator.analytics.track(wooEvent, withError: error)
538+
analytics.track(wooEvent, withError: error)
533539
}
534540

535541
// Navigate to store creation
536542
func showSiteCreation(in navigationController: UINavigationController) {
537-
ServiceLocator.analytics.track(event: .StoreCreation.loginPrologueCreateSiteTapped())
543+
analytics.track(event: .StoreCreation.loginPrologueCreateSiteTapped())
538544

539545
let coordinator = LoggedOutStoreCreationCoordinator(source: .prologue,
540546
navigationController: navigationController)

WooCommerce/Classes/Authentication/Jetpack Setup/LoginJetpackSetupCoordinator.swift

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import UIKit
22
import Yosemite
3+
import WordPressAuthenticator
34

45
/// Coordinates navigation for the Jetpack setup flow during login.
56
final class LoginJetpackSetupCoordinator: Coordinator {
@@ -10,21 +11,123 @@ final class LoginJetpackSetupCoordinator: Coordinator {
1011
private let connectionOnly: Bool
1112
private let stores: StoresManager
1213
private let analytics: Analytics
14+
private let authentication: Authentication
15+
private var storePickerCoordinator: StorePickerCoordinator?
1316

1417
init(siteURL: String,
1518
connectionOnly: Bool,
1619
navigationController: UINavigationController,
1720
stores: StoresManager = ServiceLocator.stores,
18-
analytics: Analytics = ServiceLocator.analytics) {
21+
analytics: Analytics = ServiceLocator.analytics,
22+
authentication: Authentication = ServiceLocator.authenticationManager) {
1923
self.siteURL = siteURL
2024
self.connectionOnly = connectionOnly
2125
self.navigationController = navigationController
2226
self.stores = stores
2327
self.analytics = analytics
28+
self.authentication = authentication
2429
}
2530

2631
func start() {
27-
let siteCredentialUI = SiteCredentialLoginHostingViewController(siteURL: siteURL, connectionOnly: connectionOnly)
32+
let siteCredentialUI = SiteCredentialLoginHostingViewController(
33+
siteURL: siteURL,
34+
connectionOnly: connectionOnly,
35+
onLoginSuccess: { [weak self] xmlrpc in
36+
self?.showSetupSteps(xmlrpc: xmlrpc)
37+
})
2838
navigationController.present(UINavigationController(rootViewController: siteCredentialUI), animated: true)
2939
}
3040
}
41+
42+
// MARK: Private helpers
43+
//
44+
private extension LoginJetpackSetupCoordinator {
45+
func showSetupSteps(xmlrpc: String) {
46+
let setupUI = LoginJetpackSetupHostingController(siteURL: siteURL, connectionOnly: connectionOnly, onStoreNavigation: { [weak self] connectedEmail in
47+
guard let self, let email = connectedEmail else { return }
48+
if email != self.stores.sessionManager.defaultAccount?.email {
49+
// if the user authorized Jetpack with a different account, support them to log in with that account.
50+
self.analytics.track(.loginJetpackSetupAuthorizedUsingDifferentWPCOMAccount)
51+
self.showVerifyWPComAccount(email: email, xmlrpc: xmlrpc)
52+
} else {
53+
self.showStorePickerForLogin()
54+
}
55+
56+
})
57+
guard let contentNavigationController = navigationController.presentedViewController as? UINavigationController else {
58+
// this is not likely to happen but handling this for safety
59+
return navigationController.present(UINavigationController(rootViewController: setupUI), animated: true)
60+
}
61+
contentNavigationController.setViewControllers([setupUI], animated: true)
62+
}
63+
64+
func showVerifyWPComAccount(email: String, xmlrpc: String) {
65+
WordPressAuthenticator.showVerifyEmailForWPCom(
66+
from: navigationController.presentedViewController ?? navigationController,
67+
xmlrpc: xmlrpc,
68+
connectedEmail: email,
69+
siteURL: siteURL
70+
)
71+
}
72+
73+
func showStorePickerForLogin() {
74+
storePickerCoordinator = StorePickerCoordinator(navigationController, config: .login)
75+
76+
// Tries re-syncing to get an updated store list
77+
stores.synchronizeEntities { [weak self] in
78+
guard let self = self else { return }
79+
let matcher = ULAccountMatcher()
80+
matcher.refreshStoredSites()
81+
guard let matchedSite = matcher.matchedSite(originalURL: self.siteURL) else {
82+
DDLogWarn("⚠️ Could not find \(self.siteURL) connected to the account")
83+
let topViewController = self.navigationController.presentedViewController ?? self.navigationController
84+
return self.showNoMatchedSiteAlert(from: topViewController)
85+
}
86+
87+
// dismiss the setup view
88+
self.navigationController.dismiss(animated: true)
89+
90+
// open the store picker if the matched site doesn't have Woo so the user can install it.
91+
guard matchedSite.isWooCommerceActive else {
92+
self.storePickerCoordinator?.start()
93+
return
94+
}
95+
96+
// navigate the user to the home screen.
97+
self.storePickerCoordinator?.didSelectStore(with: matchedSite.siteID, onCompletion: {})
98+
}
99+
}
100+
101+
func showNoMatchedSiteAlert(from viewController: UIViewController) {
102+
analytics.track(.loginJetpackNoMatchedSiteErrorViewed)
103+
let alert = UIAlertController(title: nil, message: Localization.noMatchSiteAlertTitle, preferredStyle: .alert)
104+
alert.addAction(UIAlertAction(title: Localization.tryAgain, style: .default, handler: { [weak self] _ in
105+
guard let self else { return }
106+
107+
self.analytics.track(.loginJetpackNoMatchedSiteErrorTryAgainButtonTapped)
108+
self.showStorePickerForLogin()
109+
}))
110+
alert.addAction(UIAlertAction(title: Localization.contactSupport, style: .default, handler: { [weak self] _ in
111+
guard let self else { return }
112+
113+
self.analytics.track(.loginJetpackNoMatchedSiteErrorContactSupportButtonTapped)
114+
self.authentication.presentSupport(from: viewController, screen: .storePicker)
115+
}))
116+
viewController.present(alert, animated: true)
117+
}
118+
}
119+
120+
private extension LoginJetpackSetupCoordinator {
121+
enum Localization {
122+
static let noMatchSiteAlertTitle = NSLocalizedString(
123+
"We cannot load the store at the moment.",
124+
comment: "Error message displayed when there is no store matching the site URL " +
125+
"that is associated with the user's account"
126+
)
127+
static let tryAgain = NSLocalizedString("Try Again", comment: "Title of the button to attempt loading the store again after Jetpack setup")
128+
static let contactSupport = NSLocalizedString(
129+
"Contact Support",
130+
comment: "Title of the button to contact support for help accessing a store after Jetpack setup"
131+
)
132+
}
133+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import SwiftUI
2+
3+
/// View to be displayed when the native Jetpack connection flow is dismissed.
4+
///
5+
struct LoginJetpackSetupInterruptedView: View {
6+
let onSupport: () -> Void
7+
let onContinue: () -> Void
8+
let onCancellation: () -> Void
9+
10+
var body: some View {
11+
VStack {
12+
HStack {
13+
Spacer()
14+
Button {
15+
onSupport()
16+
} label: {
17+
Text(Localization.help)
18+
.fontWeight(.semibold)
19+
}
20+
.buttonStyle(.plain)
21+
.foregroundColor(Color(uiColor: .accent))
22+
}
23+
.padding(.trailing, Constants.contentSpacing)
24+
.padding(.top, Constants.contentSpacing)
25+
26+
ScrollableVStack(padding: Constants.contentSpacing, spacing: Constants.contentSpacing) {
27+
Spacer()
28+
29+
VStack(spacing: Constants.contentSpacing) {
30+
Image(uiImage: .jetpackSetupInterruptedImage)
31+
Text(Localization.title)
32+
.fontWeight(.semibold)
33+
Text(Localization.message)
34+
Text(Localization.suggestion)
35+
}
36+
.font(.title3)
37+
.foregroundColor(Color(uiColor: .text))
38+
.multilineTextAlignment(.center)
39+
40+
Spacer()
41+
42+
VStack(spacing: Constants.contentSpacing) {
43+
Button {
44+
onContinue()
45+
} label: {
46+
Text(Localization.continueConnection)
47+
}
48+
.buttonStyle(PrimaryButtonStyle())
49+
50+
Button {
51+
onCancellation()
52+
} label: {
53+
Text(Localization.cancelInstallation)
54+
}
55+
.buttonStyle(SecondaryButtonStyle())
56+
}
57+
}
58+
}
59+
}
60+
}
61+
62+
extension LoginJetpackSetupInterruptedView {
63+
enum Localization {
64+
static let help = NSLocalizedString("Help", comment: "Button to contact support on the Jetpack setup interrupted screen")
65+
static let title = NSLocalizedString(
66+
"You interrupted the connection.",
67+
comment: "Title of the Jetpack setup interrupted screen"
68+
)
69+
static let message = NSLocalizedString(
70+
"Jetpack is installed, but not connected.",
71+
comment: "Message on the Jetpack setup interrupted screen"
72+
)
73+
static let suggestion = NSLocalizedString(
74+
"Please continue the connection process to access your store.",
75+
comment: "Suggestion on the Jetpack setup interrupted screen"
76+
)
77+
static let continueConnection = NSLocalizedString(
78+
"Continue Connection",
79+
comment: "Button on the Jetpack setup interrupted screen to continue the setup"
80+
)
81+
static let cancelInstallation = NSLocalizedString(
82+
"Cancel Installation",
83+
comment: "Button to cancel installation on the Jetpack setup interrupted screen"
84+
)
85+
}
86+
87+
enum Constants {
88+
static let contentSpacing: CGFloat = 16
89+
}
90+
}
91+
92+
struct LoginJetpackSetupInterruptedView_Previews: PreviewProvider {
93+
static var previews: some View {
94+
LoginJetpackSetupInterruptedView(onSupport: {}, onContinue: {}, onCancellation: {})
95+
LoginJetpackSetupInterruptedView(onSupport: {}, onContinue: {}, onCancellation: {})
96+
.previewInterfaceOrientation(.landscapeLeft)
97+
}
98+
}

0 commit comments

Comments
 (0)