Skip to content

Commit 084d5ac

Browse files
authored
Merge pull request #8127 from woocommerce/issue/8075-new-jetpack-error-screen
Login: New Jetpack required screen
2 parents a87c25e + b60842a commit 084d5ac

File tree

21 files changed

+532
-27
lines changed

21 files changed

+532
-27
lines changed

Experiments/Experiments/DefaultFeatureFlagService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
4747
.performanceMonitoringUserInteraction:
4848
// Disabled by default to avoid costs spikes, unless in internal testing builds.
4949
return buildConfig == .alpha
50+
case .nativeJetpackSetupFlow:
51+
return buildConfig == .localDeveloper || buildConfig == .alpha
5052
default:
5153
return true
5254
}

Experiments/Experiments/FeatureFlag.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,9 @@ public enum FeatureFlag: Int {
116116
///
117117
/// - Note: The app will ignore this if `performanceMonitoring` is `false`.
118118
case performanceMonitoringViewController
119+
120+
/// Temporary feature flag for the native Jetpack setup flow.
121+
/// TODO-8075: replace this with A/B test.
122+
///
123+
case nativeJetpackSetupFlow
119124
}

WooCommerce/Classes/Authentication/AuthenticationManager.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,12 @@ class AuthenticationManager: Authentication {
4141
///
4242
private let storageManager: StorageManagerType
4343

44+
private let featureFlagService: FeatureFlagService
45+
4446
init(storageManager: StorageManagerType = ServiceLocator.storageManager,
4547
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) {
4648
self.storageManager = storageManager
49+
self.featureFlagService = featureFlagService
4750
}
4851

4952
/// Initializes the WordPress Authenticator.
@@ -723,6 +726,18 @@ private extension AuthenticationManager {
723726
return ULErrorViewController(viewModel: viewModel)
724727
}
725728

729+
/// The error screen to be displayed when Jetpack setup for a site is required.
730+
/// This is the entry point to the native Jetpack setup flow.
731+
///
732+
func jetpackSetupUI(for siteURL: String,
733+
connectionMissingOnly: Bool,
734+
in navigationController: UINavigationController) -> UIViewController {
735+
let viewModel = JetpackSetupRequiredViewModel(siteURL: siteURL,
736+
connectionOnly: connectionMissingOnly)
737+
let jetpackSetupUI = ULErrorViewController(viewModel: viewModel)
738+
return jetpackSetupUI
739+
}
740+
726741
/// Appropriate error to display for a site when entered from the site discovery flow.
727742
/// More about this flow: pe5sF9-mz-p2
728743
///
@@ -739,6 +754,14 @@ private extension AuthenticationManager {
739754
return accountMismatchUI(for: site.url, siteCredentials: nil, with: matcher, in: navigationController)
740755
}
741756

757+
// Shows the native Jetpack flow during the site discovery flow.
758+
// TODO-8075: replace feature flag with A/B testing
759+
if featureFlagService.isFeatureFlagEnabled(.nativeJetpackSetupFlow) {
760+
return jetpackSetupUI(for: site.url,
761+
connectionMissingOnly: site.hasJetpack && site.isJetpackActive,
762+
in: navigationController)
763+
}
764+
742765
/// Jetpack is required. Present an error if we don't detect a valid installation.
743766
guard site.hasJetpack && site.isJetpackActive else {
744767
return jetpackErrorUI(for: site.url, with: matcher, in: navigationController)

WooCommerce/Classes/Authentication/Epilogue/EmptyStoresTableViewCell.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,21 @@ final class EmptyStoresTableViewCell: UITableViewCell {
1818
}
1919
}
2020

21+
@IBOutlet private var subtitleLabel: UILabel! {
22+
didSet {
23+
subtitleLabel.textColor = .secondaryLabel
24+
subtitleLabel.font = .body
25+
subtitleLabel.text = Localization.subtitle
26+
}
27+
}
28+
2129
@IBOutlet private weak var stackView: UIStackView!
2230
@IBOutlet private weak var emptyStoresImageView: UIImageView!
2331
@IBOutlet private weak var removeAppleIDAccessButton: UIButton!
2432

2533
override func awakeFromNib() {
2634
super.awakeFromNib()
2735

28-
configureBackground()
2936
configureStackView()
3037
configureImageView()
3138
configureRemoveAppleIDAccessButton()
@@ -39,9 +46,6 @@ final class EmptyStoresTableViewCell: UITableViewCell {
3946

4047

4148
private extension EmptyStoresTableViewCell {
42-
func configureBackground() {
43-
applyDefaultBackgroundStyle()
44-
}
4549

4650
func configureStackView() {
4751
stackView.spacing = 24
@@ -50,6 +54,7 @@ private extension EmptyStoresTableViewCell {
5054

5155
func configureImageView() {
5256
emptyStoresImageView.contentMode = .scaleAspectFit
57+
emptyStoresImageView.image = .emptyStorePickerImage
5358
}
5459

5560
func configureRemoveAppleIDAccessButton() {
@@ -68,7 +73,9 @@ private extension EmptyStoresTableViewCell {
6873
comment: "Link on the store picker for users who signed in with Apple to close their WordPress.com account."
6974
)
7075
static let legend =
71-
NSLocalizedString("Your account isn’t connected to any WooCommerce stores.",
76+
NSLocalizedString("Create your first store",
7277
comment: "Displayed during the Login flow, whenever the user has no woo stores associated.")
78+
static let subtitle = NSLocalizedString("Quickly get up and selling with a beautiful online store.",
79+
comment: "Subtitle displayed during the Login flow, whenever the user has no woo stores associated.")
7380
}
7481
}

WooCommerce/Classes/Authentication/Epilogue/EmptyStoresTableViewCell.xib

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21179.7" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
33
<device id="retina4_7" orientation="portrait" appearance="light"/>
44
<dependencies>
55
<deployment identifier="iOS"/>
6-
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21169.4"/>
6+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
77
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
88
</dependencies>
99
<objects>
@@ -20,16 +20,27 @@
2020
<rect key="frame" x="0.0" y="0.0" width="375" height="222"/>
2121
<subviews>
2222
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="woo-no-store" translatesAutoresizingMaskIntoConstraints="NO" id="uuh-xp-DA5">
23-
<rect key="frame" x="0.0" y="0.0" width="375" height="152"/>
23+
<rect key="frame" x="0.0" y="0.0" width="375" height="138.5"/>
2424
</imageView>
25-
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="751" text="Label" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bPV-bJ-GBz">
26-
<rect key="frame" x="0.0" y="152" width="375" height="20.5"/>
27-
<fontDescription key="fontDescription" type="system" pointSize="17"/>
28-
<nil key="textColor"/>
29-
<nil key="highlightedColor"/>
30-
</label>
25+
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="JZd-ud-Vc8">
26+
<rect key="frame" x="0.0" y="138.5" width="375" height="49"/>
27+
<subviews>
28+
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="751" text="Label" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bPV-bJ-GBz">
29+
<rect key="frame" x="0.0" y="0.0" width="375" height="20.5"/>
30+
<fontDescription key="fontDescription" type="system" pointSize="17"/>
31+
<nil key="textColor"/>
32+
<nil key="highlightedColor"/>
33+
</label>
34+
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="751" text="Label" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Oil-C8-RKM">
35+
<rect key="frame" x="0.0" y="28.5" width="375" height="20.5"/>
36+
<fontDescription key="fontDescription" type="system" pointSize="17"/>
37+
<nil key="textColor"/>
38+
<nil key="highlightedColor"/>
39+
</label>
40+
</subviews>
41+
</stackView>
3142
<button opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="751" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="XF9-r3-Vq9" userLabel="Remove Apple ID Access Button">
32-
<rect key="frame" x="0.0" y="172.5" width="375" height="49.5"/>
43+
<rect key="frame" x="0.0" y="187.5" width="375" height="34.5"/>
3344
<state key="normal" title="Button"/>
3445
<buttonConfiguration key="configuration" style="plain" title="Button"/>
3546
</button>
@@ -50,6 +61,7 @@
5061
<outlet property="legendLabel" destination="bPV-bJ-GBz" id="sQX-ir-ghQ"/>
5162
<outlet property="removeAppleIDAccessButton" destination="XF9-r3-Vq9" id="sLP-Jl-0hK"/>
5263
<outlet property="stackView" destination="pky-np-rBc" id="vRx-dY-Ma9"/>
64+
<outlet property="subtitleLabel" destination="Oil-C8-RKM" id="9VO-5Z-BNb"/>
5365
</connections>
5466
<point key="canvasLocation" x="-162.5" y="-12"/>
5567
</tableViewCell>
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import UIKit
2+
3+
/// Configuration and actions for an ULErrorViewController,
4+
/// modeling an error when Jetpack is not installed or is not connected
5+
/// Displayed as an entry point to the native Jetpack setup flow.
6+
///
7+
struct JetpackSetupRequiredViewModel: ULErrorViewModel {
8+
private let siteURL: String
9+
private let connectionOnly: Bool
10+
private let authentication: Authentication
11+
private let analytics: Analytics
12+
13+
init(siteURL: String,
14+
connectionOnly: Bool,
15+
authentication: Authentication = ServiceLocator.authenticationManager,
16+
analytics: Analytics = ServiceLocator.analytics) {
17+
self.connectionOnly = connectionOnly
18+
self.siteURL = siteURL
19+
self.authentication = authentication
20+
self.analytics = analytics
21+
}
22+
23+
// MARK: - Data and configuration
24+
let title: String? = Localization.title
25+
26+
var image: UIImage {
27+
connectionOnly ? .jetpackConnectionImage : .jetpackSetupImage
28+
}
29+
30+
var text: NSAttributedString {
31+
let font: UIFont = .body
32+
let boldFont: UIFont = font.bold
33+
34+
let boldSiteAddress = NSAttributedString(string: siteURL.trimHTTPScheme(),
35+
attributes: [.font: boldFont])
36+
let message = NSMutableAttributedString(string: connectionOnly ? Localization.connectionErrorMessage : Localization.setupErrorMessage)
37+
38+
message.replaceFirstOccurrence(of: "%@", with: boldSiteAddress)
39+
40+
let subtitle = connectionOnly ? Localization.connectionSubtitle : Localization.setupSubtitle
41+
let subtitleAttributedString = NSAttributedString(string: "\n\n" + subtitle,
42+
attributes: [.font: UIFont.body,
43+
.foregroundColor: UIColor.secondaryLabel])
44+
message.append(subtitleAttributedString)
45+
return message
46+
}
47+
48+
let isAuxiliaryButtonHidden = true
49+
50+
let auxiliaryButtonTitle = ""
51+
52+
var primaryButtonTitle: String {
53+
connectionOnly ? Localization.connectJetpack : Localization.installJetpack
54+
}
55+
56+
let secondaryButtonTitle = ""
57+
58+
let isSecondaryButtonHidden = true
59+
60+
// Configures `Help` button title
61+
let rightBarButtonItemTitle: String? = Localization.helpBarButtonItemTitle
62+
63+
var termsLabelText: NSAttributedString? {
64+
let content = String.localizedStringWithFormat(Localization.termsContent, Localization.termsOfService, Localization.shareDetails)
65+
let paragraph = NSMutableParagraphStyle()
66+
paragraph.alignment = .center
67+
68+
let mutableAttributedText = NSMutableAttributedString(
69+
string: content,
70+
attributes: [.font: UIFont.caption1,
71+
.foregroundColor: UIColor.text,
72+
.paragraphStyle: paragraph]
73+
)
74+
75+
mutableAttributedText.setAsLink(textToFind: Localization.termsOfService,
76+
linkURL: Links.jetpackTerms + self.siteURL)
77+
mutableAttributedText.setAsLink(textToFind: Localization.shareDetails,
78+
linkURL: Links.jetpackShareDetails + self.siteURL)
79+
return mutableAttributedText
80+
}
81+
82+
func viewDidLoad(_ viewController: UIViewController?) {
83+
// TODO: add tracks
84+
}
85+
86+
func didTapPrimaryButton(in viewController: UIViewController?) {
87+
// TODO: handle Jetpack setup natively
88+
}
89+
90+
func didTapSecondaryButton(in viewController: UIViewController?) {
91+
// no-op
92+
}
93+
94+
func didTapAuxiliaryButton(in viewController: UIViewController?) {
95+
// no-op
96+
}
97+
98+
func didTapRightBarButtonItem(in viewController: UIViewController?) {
99+
guard let viewController = viewController else {
100+
return
101+
}
102+
authentication.presentSupport(from: viewController, screen: .jetpackRequired)
103+
}
104+
}
105+
106+
extension JetpackSetupRequiredViewModel {
107+
enum Localization {
108+
static let title = NSLocalizedString("Connect Store", comment: "Title of the Jetpack setup required screen")
109+
static let installJetpack = NSLocalizedString(
110+
"Install Jetpack",
111+
comment: "Button to install Jetpack from the Jetpack setup required screen"
112+
)
113+
static let connectJetpack = NSLocalizedString(
114+
"Connect Jetpack",
115+
comment: "Button to authorize Jetpack connection from the Jetpack setup required screen"
116+
)
117+
static let setupErrorMessage = NSLocalizedString(
118+
"To use this app for %@ you'll need the free Jetpack plugin installed and connected on your store.",
119+
comment: "Error message on the Jetpack setup required screen." +
120+
"Reads like: To use this app for test.com you'll need..."
121+
)
122+
static let connectionErrorMessage = NSLocalizedString(
123+
"To use this app for %@ you'll need to connect your store to Jetpack.",
124+
comment: "Error message on the Jetpack setup required screen when Jetpack connection is missing." +
125+
"Reads like: To use this app for test.com you'll need..."
126+
)
127+
static let setupSubtitle = NSLocalizedString(
128+
"You’ll need your store credentials to begin the installation.",
129+
comment: "Subtitle on the Jetpack setup required screen"
130+
)
131+
static let connectionSubtitle = NSLocalizedString(
132+
"You’ll need your store credentials to begin the connection.",
133+
comment: "Subtitle on the Jetpack setup required screen when only Jetpack connection is missing"
134+
)
135+
static let helpBarButtonItemTitle = NSLocalizedString("Help", comment: "Help button on Jetpack setup required screen.")
136+
static let termsContent = NSLocalizedString(
137+
"By tapping the Connect Jetpack button, you agree to our %1$@ and to %2$@ with WordPress.com.",
138+
comment: "Content of the label at the end of the Jetpack setup required screen. " +
139+
"Reads like: By tapping the Connect Jetpack button, you agree to our Terms of Service and to share details with WordPress.com."
140+
)
141+
static let termsOfService = NSLocalizedString(
142+
"Terms of Service",
143+
comment: "The terms to be agreed upon when tapping the Connect Jetpack button on the Jetpack setup required screen."
144+
)
145+
static let shareDetails = NSLocalizedString(
146+
"share details",
147+
comment: "The action to be agreed upon when tapping the Connect Jetpack button on the Jetpack setup required screen."
148+
)
149+
}
150+
151+
enum Links {
152+
static let jetpackTerms = "https://jetpack.com/redirect/?source=wpcom-tos&site="
153+
static let jetpackShareDetails = "https://jetpack.com/redirect/?source=jetpack-support-what-data-does-jetpack-sync&site="
154+
}
155+
}

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ final class ULErrorViewController: UIViewController {
2424
@IBOutlet private weak var secondaryButton: UIButton!
2525
@IBOutlet private weak var imageView: UIImageView!
2626
@IBOutlet private weak var errorMessage: UILabel!
27-
27+
@IBOutlet private weak var termsLabel: UITextView!
2828

2929
/// Constraints on the view containing the action buttons
3030
/// and the stack view containing the image and error text
@@ -64,6 +64,7 @@ final class ULErrorViewController: UIViewController {
6464

6565
configurePrimaryButton()
6666
configureSecondaryButton()
67+
configureTermsLabel()
6768

6869
configureButtonLabels()
6970

@@ -133,6 +134,20 @@ private extension ULErrorViewController {
133134
}
134135
}
135136

137+
func configureTermsLabel() {
138+
let linkAttributes: [NSAttributedString.Key: Any] = [
139+
NSAttributedString.Key.foregroundColor: UIColor.accent,
140+
NSAttributedString.Key.underlineColor: UIColor.accent,
141+
NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue
142+
]
143+
termsLabel.linkTextAttributes = linkAttributes
144+
termsLabel.isSelectable = true
145+
termsLabel.isHidden = viewModel.termsLabelText == nil
146+
if let text = viewModel.termsLabelText {
147+
termsLabel.attributedText = text
148+
}
149+
}
150+
136151
func configureAuxiliaryView() {
137152
guard let auxiliaryView = viewModel.auxiliaryView else {
138153
return
@@ -264,4 +279,8 @@ extension ULErrorViewController {
264279
func secondaryActionButton() -> UIButton {
265280
return secondaryButton
266281
}
282+
283+
func getTermsLabel() -> UITextView {
284+
return termsLabel
285+
}
267286
}

0 commit comments

Comments
 (0)