Skip to content

Commit dc1a7a2

Browse files
authored
Merge pull request #5476 from woocommerce/issue/5365-install-jetpack-steps-UI
JCP: Install Jetpack steps UI
2 parents f322d9c + 588c010 commit dc1a7a2

File tree

17 files changed

+381
-23
lines changed

17 files changed

+381
-23
lines changed

WooCommerce/Classes/Extensions/UIImage+Woo.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,18 @@ extension UIImage {
102102
.imageWithTintColor(.listBackground)!
103103
}
104104

105+
/// Green circle with checkmark
106+
///
107+
static var checkCircleImage: UIImage {
108+
return UIImage(named: "check-circle-done")!
109+
}
110+
111+
/// Circle without checkmark
112+
///
113+
static var checkEmptyCircleImage: UIImage {
114+
return UIImage(named: "check-circle-empty")!
115+
}
116+
105117
/// WooCommerce Styled Checkmark
106118
///
107119
static var checkmarkStyledImage: UIImage {
@@ -145,6 +157,12 @@ extension UIImage {
145157
return UIImage.gridicon(.cloudOutline)
146158
}
147159

160+
/// Connection Icon
161+
///
162+
static var connectionImage: UIImage {
163+
return UIImage(named: "icon-connection")!
164+
}
165+
148166
/// Gear Icon - used in `UIBarButtonItem`
149167
///
150168
static var gearBarButtonItemImage: UIImage {

WooCommerce/Classes/View Modifiers/View+Conditionals.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,17 @@ extension View {
1010
}
1111
return self
1212
}
13+
14+
/// Applies the given transform if the given condition evaluates to `true`.
15+
/// - Parameters:
16+
/// - condition: The condition to evaluate.
17+
/// - transform: The transform to apply to the source `View`.
18+
/// - Returns: Either the original `View` or the modified `View` if the condition is `true`.
19+
@ViewBuilder func `if`<Content: View>(_ condition: @autoclosure () -> Bool, transform: (Self) -> Content) -> some View {
20+
if condition() {
21+
transform(self)
22+
} else {
23+
self
24+
}
25+
}
1326
}

WooCommerce/Classes/ViewRelated/Dashboard/JetpackInstall/JetpackInstallIntroView.swift

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,20 @@
11
import SwiftUI
22

3-
/// Hosting controller wrapper for `JetpackInstallIntroView`
4-
///
5-
final class JetpackInstallHostingController: UIHostingController<JetpackInstallIntroView> {
6-
init(siteURL: String) {
7-
super.init(rootView: JetpackInstallIntroView(siteURL: siteURL))
8-
}
9-
10-
required dynamic init?(coder aDecoder: NSCoder) {
11-
fatalError("init(coder:) has not been implemented")
12-
}
13-
14-
func setDismissAction(_ dismissAction: @escaping () -> Void) {
15-
rootView.dismissAction = dismissAction
16-
}
17-
}
18-
193
/// Displays the intro view for the Jetpack install flow.
204
///
215
struct JetpackInstallIntroView: View {
226
// Closure invoked when Close button is tapped
23-
var dismissAction: () -> Void = {}
7+
private let dismissAction: () -> Void
8+
9+
// Closure invoked when Get Started button is tapped
10+
private let startAction: () -> Void
2411

2512
private let siteURL: String
2613

27-
init(siteURL: String) {
14+
init(siteURL: String, dismissAction: @escaping () -> Void, startAction: @escaping () -> Void) {
2815
self.siteURL = siteURL
16+
self.dismissAction = dismissAction
17+
self.startAction = startAction
2918
}
3019

3120
private var descriptionAttributedString: NSAttributedString {
@@ -82,9 +71,7 @@ struct JetpackInstallIntroView: View {
8271
Spacer()
8372

8473
// Primary Button to install Jetpack
85-
Button(Localization.installAction, action: {
86-
// TODO: Show main install screen
87-
})
74+
Button(Localization.installAction, action: startAction)
8875
.buttonStyle(PrimaryButtonStyle())
8976
.fixedSize(horizontal: false, vertical: true)
9077
.padding(.horizontal, Constants.actionButtonMargin)
@@ -115,11 +102,11 @@ private extension JetpackInstallIntroView {
115102

116103
struct JetpackInstallIntroView_Previews: PreviewProvider {
117104
static var previews: some View {
118-
JetpackInstallIntroView(siteURL: "automattic.com")
105+
JetpackInstallIntroView(siteURL: "automattic.com", dismissAction: {}, startAction: {})
119106
.preferredColorScheme(.light)
120107
.previewLayout(.fixed(width: 414, height: 780))
121108

122-
JetpackInstallIntroView(siteURL: "automattic.com")
109+
JetpackInstallIntroView(siteURL: "automattic.com", dismissAction: {}, startAction: {})
123110
.preferredColorScheme(.dark)
124111
.previewLayout(.fixed(width: 800, height: 400))
125112
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Foundation
2+
3+
/// Enum respresenting steps for installing Jetpack for a site.
4+
/// Used for displaying steps on JetpackInstallStepsView.
5+
///
6+
enum JetpackInstallStep: Int, CaseIterable {
7+
case installation
8+
case activation
9+
case connection
10+
case done
11+
}
12+
13+
extension JetpackInstallStep: Identifiable {
14+
var id: Int {
15+
rawValue
16+
}
17+
}
18+
19+
extension JetpackInstallStep: Comparable {
20+
static func < (lhs: JetpackInstallStep, rhs: JetpackInstallStep) -> Bool {
21+
lhs.rawValue < rhs.rawValue
22+
}
23+
}
24+
25+
extension JetpackInstallStep {
26+
27+
/// Display text of the step
28+
///
29+
var title: String {
30+
switch self {
31+
case .installation:
32+
return Localization.installationStep
33+
case .activation:
34+
return Localization.activationStep
35+
case .connection:
36+
return Localization.connectionStep
37+
case .done:
38+
return Localization.finalStep
39+
}
40+
}
41+
42+
private enum Localization {
43+
static let installationStep = NSLocalizedString("Installing Jetpack", comment: "Name of installing Jetpack plugin step")
44+
static let activationStep = NSLocalizedString("Activating", comment: "Name of the activation Jetpack plugin step")
45+
static let connectionStep = NSLocalizedString("Connecting your store", comment: "Name of the step to connect the store to Jetpack")
46+
static let finalStep = NSLocalizedString("All done", comment: "Name of final step in Install Jetpack flow.")
47+
}
48+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import SwiftUI
2+
3+
struct JetpackInstallStepsView: View {
4+
// Closure invoked when Done button is tapped
5+
private let dismissAction: () -> Void
6+
7+
/// The site for which Jetpack should be installed
8+
private let siteURL: String
9+
10+
/// Scale of the view based on accessibility changes
11+
@ScaledMetric private var scale: CGFloat = 1.0
12+
13+
/// TODO-5365: Set real step, maybe as an @Published variable from an observable object.
14+
@State private var currentStep: JetpackInstallStep = .activation
15+
16+
/// Attributed string for the description text
17+
private var descriptionAttributedString: NSAttributedString {
18+
let font: UIFont = .body
19+
let boldFont: UIFont = font.bold
20+
let siteName = siteURL.trimHTTPScheme()
21+
22+
let attributedString = NSMutableAttributedString(
23+
string: String(format: Localization.installDescription, siteName),
24+
attributes: [.font: font,
25+
.foregroundColor: UIColor.text.withAlphaComponent(0.8)
26+
]
27+
)
28+
let boldSiteAddress = NSAttributedString(string: siteName, attributes: [.font: boldFont, .foregroundColor: UIColor.text])
29+
attributedString.replaceFirstOccurrence(of: siteName, with: boldSiteAddress)
30+
return attributedString
31+
}
32+
33+
init(siteURL: String, dismissAction: @escaping () -> Void) {
34+
self.siteURL = siteURL
35+
self.dismissAction = dismissAction
36+
}
37+
38+
var body: some View {
39+
VStack {
40+
// Main content
41+
VStack(alignment: .leading, spacing: Constants.contentSpacing) {
42+
// Header
43+
HStack(spacing: Constants.headerContentSpacing) {
44+
Image(uiImage: .jetpackGreenLogoImage)
45+
.resizable()
46+
.frame(width: Constants.logoSize * scale, height: Constants.logoSize * scale)
47+
Image(uiImage: .connectionImage)
48+
.resizable()
49+
.flipsForRightToLeftLayoutDirection(true)
50+
.frame(width: Constants.connectionIconSize * scale, height: Constants.connectionIconSize * scale)
51+
52+
if let image = UIImage.wooLogoImage(tintColor: .white) {
53+
Circle()
54+
.foregroundColor(Color(.withColorStudio(.wooCommercePurple, shade: .shade60)))
55+
.frame(width: Constants.logoSize * scale, height: Constants.logoSize * scale)
56+
.overlay(
57+
Image(uiImage: image)
58+
.resizable()
59+
.frame(width: Constants.wooIconSize.width * scale, height: Constants.wooIconSize.height * scale)
60+
)
61+
}
62+
63+
Spacer()
64+
}
65+
.padding(.top, Constants.contentTopMargin)
66+
67+
// Title and description
68+
VStack(alignment: .leading, spacing: Constants.textSpacing) {
69+
Text(Localization.installTitle)
70+
.font(.largeTitle)
71+
.bold()
72+
.foregroundColor(Color(.text))
73+
74+
AttributedText(descriptionAttributedString)
75+
}
76+
77+
// Install steps
78+
VStack(alignment: .leading, spacing: Constants.stepItemsVerticalSpacing) {
79+
ForEach(JetpackInstallStep.allCases) { step in
80+
HStack(spacing: Constants.stepItemHorizontalSpacing) {
81+
if step == currentStep, step != .done {
82+
ActivityIndicator(isAnimating: .constant(true), style: .medium)
83+
} else if step > currentStep {
84+
Image(uiImage: .checkEmptyCircleImage)
85+
.resizable()
86+
.frame(width: Constants.stepImageSize * scale, height: Constants.stepImageSize * scale)
87+
} else {
88+
Image(uiImage: .checkCircleImage)
89+
.resizable()
90+
.frame(width: Constants.stepImageSize * scale, height: Constants.stepImageSize * scale)
91+
}
92+
93+
Text(step.title)
94+
.font(.body)
95+
.if(step <= currentStep) {
96+
$0.bold()
97+
}
98+
.foregroundColor(Color(.text))
99+
.opacity(step <= currentStep ? 1 : 0.5)
100+
}
101+
}
102+
}
103+
}
104+
.padding(.horizontal, Constants.contentHorizontalMargin)
105+
.scrollVerticallyIfNeeded()
106+
107+
Spacer()
108+
109+
// Done Button to dismiss Install Jetpack
110+
Button(Localization.doneButton, action: dismissAction)
111+
.buttonStyle(PrimaryButtonStyle())
112+
.fixedSize(horizontal: false, vertical: true)
113+
.padding(Constants.actionButtonMargin)
114+
}
115+
}
116+
}
117+
118+
private extension JetpackInstallStepsView {
119+
enum Constants {
120+
static let headerContentSpacing: CGFloat = 8
121+
static let contentTopMargin: CGFloat = 80
122+
static let contentHorizontalMargin: CGFloat = 40
123+
static let contentSpacing: CGFloat = 32
124+
static let logoSize: CGFloat = 40
125+
static let wooIconSize: CGSize = .init(width: 30, height: 18)
126+
static let connectionIconSize: CGFloat = 10
127+
static let textSpacing: CGFloat = 12
128+
static let actionButtonMargin: CGFloat = 16
129+
static let stepItemHorizontalSpacing: CGFloat = 24
130+
static let stepItemsVerticalSpacing: CGFloat = 20
131+
static let stepImageSize: CGFloat = 24
132+
}
133+
134+
enum Localization {
135+
static let installTitle = NSLocalizedString("Install Jetpack", comment: "Title of the Install Jetpack view")
136+
static let installDescription = NSLocalizedString("Please wait while we connect your site %1$@ with Jetpack.",
137+
comment: "Message on the Jetpack Install Progress screen. The %1$@ is the site address.")
138+
static let doneButton = NSLocalizedString("Done", comment: "Done button on the Jetpack Install Progress screen.")
139+
}
140+
}
141+
142+
struct JetpackInstallStepsView_Previews: PreviewProvider {
143+
static var previews: some View {
144+
JetpackInstallStepsView(siteURL: "automattic.com", dismissAction: {})
145+
.preferredColorScheme(.light)
146+
.previewLayout(.fixed(width: 414, height: 780))
147+
148+
JetpackInstallStepsView(siteURL: "automattic.com", dismissAction: {})
149+
.preferredColorScheme(.dark)
150+
.previewLayout(.fixed(width: 414, height: 780))
151+
}
152+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import SwiftUI
2+
3+
/// Hosting controller wrapper for `JetpackInstallIntroView`
4+
///
5+
final class JetpackInstallHostingController: UIHostingController<JetpackInstallView> {
6+
init(siteURL: String) {
7+
super.init(rootView: JetpackInstallView(siteURL: siteURL))
8+
}
9+
10+
required dynamic init?(coder aDecoder: NSCoder) {
11+
fatalError("init(coder:) has not been implemented")
12+
}
13+
14+
func setDismissAction(_ dismissAction: @escaping () -> Void) {
15+
rootView.dismissAction = dismissAction
16+
}
17+
}
18+
19+
/// Displays Jetpack Install flow.
20+
///
21+
struct JetpackInstallView: View {
22+
// Closure invoked when Close button is tapped
23+
var dismissAction: () -> Void = {}
24+
25+
private let siteURL: String
26+
27+
@State private var hasStarted = false
28+
29+
init(siteURL: String) {
30+
self.siteURL = siteURL
31+
}
32+
33+
var body: some View {
34+
if hasStarted {
35+
JetpackInstallStepsView(siteURL: siteURL, dismissAction: dismissAction)
36+
} else {
37+
JetpackInstallIntroView(siteURL: siteURL, dismissAction: dismissAction) {
38+
hasStarted = true
39+
}
40+
}
41+
}
42+
}
43+
44+
struct JetpackInstallView_Previews: PreviewProvider {
45+
static var previews: some View {
46+
JetpackInstallView(siteURL: "automattic.com")
47+
.preferredColorScheme(.light)
48+
.previewLayout(.fixed(width: 414, height: 780))
49+
}
50+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "check-circle-done.pdf",
5+
"idiom" : "universal"
6+
},
7+
{
8+
"appearances" : [
9+
{
10+
"appearance" : "luminosity",
11+
"value" : "dark"
12+
}
13+
],
14+
"filename" : "check-circle-done-dark.pdf",
15+
"idiom" : "universal"
16+
}
17+
],
18+
"info" : {
19+
"author" : "xcode",
20+
"version" : 1
21+
}
22+
}
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)