Skip to content

Commit d6d3eaf

Browse files
committed
Create StoreCreationPlanView for WPCOM plan screen for purchase.
1 parent 49e5ae3 commit d6d3eaf

File tree

3 files changed

+288
-0
lines changed

3 files changed

+288
-0
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import SwiftUI
2+
3+
/// Hosting controller that wraps the `StoreCreationPlanView`.
4+
final class StoreCreationPlanHostingController: UIHostingController<StoreCreationPlanView> {
5+
private let onPurchase: () -> Void
6+
private let onClose: () -> Void
7+
8+
init(viewModel: StoreCreationPlanViewModel,
9+
onPurchase: @escaping () -> Void,
10+
onClose: @escaping () -> Void) {
11+
self.onPurchase = onPurchase
12+
self.onClose = onClose
13+
super.init(rootView: StoreCreationPlanView(viewModel: viewModel))
14+
15+
rootView.onPurchase = { [weak self] in
16+
self?.onPurchase()
17+
}
18+
}
19+
20+
@available(*, unavailable)
21+
required dynamic init?(coder aDecoder: NSCoder) {
22+
fatalError("init(coder:) has not been implemented")
23+
}
24+
25+
override func viewDidLoad() {
26+
super.viewDidLoad()
27+
28+
configureNavigationBarAppearance()
29+
}
30+
31+
/// Shows a transparent navigation bar without a bottom border and with a close button to dismiss.
32+
func configureNavigationBarAppearance() {
33+
addCloseNavigationBarButton(target: self, action: #selector(closeButtonTapped))
34+
35+
let appearance = UINavigationBarAppearance()
36+
appearance.configureWithTransparentBackground()
37+
appearance.backgroundColor = .withColorStudio(.wooCommercePurple, shade: .shade90)
38+
39+
navigationItem.standardAppearance = appearance
40+
navigationItem.scrollEdgeAppearance = appearance
41+
navigationItem.compactAppearance = appearance
42+
}
43+
44+
@objc private func closeButtonTapped() {
45+
onClose()
46+
}
47+
}
48+
49+
/// Displays the WPCOM eCommerce plan for purchase during the store creation flow.
50+
struct StoreCreationPlanView: View {
51+
/// Set in the hosting controller.
52+
var onPurchase: (() -> Void) = {}
53+
54+
let viewModel: StoreCreationPlanViewModel
55+
56+
var body: some View {
57+
VStack(alignment: .leading, spacing: 0) {
58+
ScrollView {
59+
VStack(alignment: .leading, spacing: 0) {
60+
HStack(alignment: .center) {
61+
VStack(alignment: .leading, spacing: 12) {
62+
// Plan name.
63+
Text(Localization.planTitle)
64+
.fontWeight(.semibold)
65+
.font(.title3)
66+
.foregroundColor(.white)
67+
68+
// Price information.
69+
HStack(alignment: .bottom) {
70+
Text(viewModel.plan.displayPrice)
71+
.fontWeight(.bold)
72+
.foregroundColor(.white)
73+
.largeTitleStyle()
74+
Text(Localization.priceDuration)
75+
.foregroundColor(Color(.secondaryLabel))
76+
.bodyStyle()
77+
}
78+
}
79+
.padding(.horizontal, insets: .init(top: 0, leading: 24, bottom: 0, trailing: 0))
80+
81+
Spacer()
82+
83+
Image(uiImage: .storeCreationPlanImage)
84+
}
85+
86+
Divider()
87+
.frame(height: Layout.dividerHeight)
88+
.foregroundColor(Color(Layout.dividerColor))
89+
.padding(.horizontal, insets: Layout.defaultPadding)
90+
91+
VStack(alignment: .leading, spacing: 0) {
92+
Spacer()
93+
.frame(height: 8)
94+
95+
// Header label.
96+
Text(Localization.subtitle)
97+
.fontWeight(.bold)
98+
.foregroundColor(Color(.white))
99+
.titleStyle()
100+
101+
Spacer()
102+
.frame(height: 16)
103+
104+
// Powered by WPCOM.
105+
HStack(spacing: 5) {
106+
Text(Localization.poweredByWPCOMPrompt)
107+
.foregroundColor(Color(.secondaryLabel))
108+
.footnoteStyle()
109+
Image(uiImage: .wpcomLogoImage)
110+
}
111+
112+
Spacer()
113+
.frame(height: 32)
114+
115+
// Plan features.
116+
VStack(alignment: .leading, spacing: 16) {
117+
ForEach(viewModel.features, id: \.title) { feature in
118+
HStack(spacing: 12) {
119+
Image(uiImage: feature.icon)
120+
.renderingMode(.template)
121+
.foregroundColor(Color(.wooCommercePurple(.shade90)))
122+
Text(feature.title)
123+
.foregroundColor(Color(.label))
124+
.bodyStyle()
125+
}
126+
}
127+
}
128+
}
129+
.padding(Layout.defaultPadding)
130+
}
131+
}
132+
133+
VStack(spacing: 0) {
134+
Divider()
135+
.frame(height: Layout.dividerHeight)
136+
.foregroundColor(Color(Layout.dividerColor))
137+
138+
// Continue button.
139+
Button(String(format: Localization.continueButtonTitleFormat, viewModel.plan.displayPrice)) {
140+
onPurchase()
141+
}
142+
.buttonStyle(PrimaryButtonStyle())
143+
.padding(Layout.defaultButtonPadding)
144+
145+
// Refund information.
146+
Text(Localization.refundableNote)
147+
.multilineTextAlignment(.center)
148+
.foregroundColor(Color(.secondaryLabel))
149+
.bodyStyle()
150+
151+
Spacer()
152+
.frame(height: 24)
153+
}
154+
}
155+
.background(Color(.withColorStudio(.wooCommercePurple, shade: .shade90)))
156+
// This screen is using the dark theme for both light and dark modes.
157+
.preferredColorScheme(.dark)
158+
}
159+
}
160+
161+
private extension StoreCreationPlanView {
162+
enum Layout {
163+
static let dividerHeight: CGFloat = 1
164+
static let defaultPadding: EdgeInsets = .init(top: 16, leading: 16, bottom: 16, trailing: 16)
165+
static let defaultButtonPadding: EdgeInsets = .init(top: 16, leading: 16, bottom: 16, trailing: 16)
166+
static let dividerColor: UIColor = .separator
167+
}
168+
169+
enum Localization {
170+
static let planTitle = NSLocalizedString(
171+
"eCommerce",
172+
comment: "Title of the store creation plan on the plan screen.")
173+
static let priceDuration = NSLocalizedString(
174+
"/month",
175+
comment: "The text is preceded by the monthly price on the store creation plan screen.")
176+
static let subtitle = NSLocalizedString(
177+
"All the featues you need, already built in",
178+
comment: "Subtitle of the store creation plan screen.")
179+
static let poweredByWPCOMPrompt = NSLocalizedString(
180+
"Powered by",
181+
comment: "The text is followed by a WordPress.com logo on the store creation plan screen.")
182+
static let continueButtonTitleFormat = NSLocalizedString(
183+
"Create Store for %1$@/month",
184+
comment: "Title of the button on the store creation plan view to purchase the plan. " +
185+
"%1$@ is replaced by the monthly price."
186+
)
187+
static let refundableNote = NSLocalizedString(
188+
"There’s no risk, you can cancel for a full refund within 30 days.",
189+
comment: "Refund policy under the purchase button on the store creation plan screen."
190+
)
191+
}
192+
}
193+
194+
#if DEBUG
195+
196+
/// Only used for `StoreCreationPlanView` preview.
197+
private struct Plan: WPComPlanProduct {
198+
let displayName: String
199+
let description: String
200+
let id: String
201+
let displayPrice: String
202+
}
203+
204+
struct StoreCreationPlanView_Previews: PreviewProvider {
205+
static var previews: some View {
206+
StoreCreationPlanView(viewModel:
207+
.init(plan: Plan(displayName: "Debug Monthly",
208+
description: "1 Month of Debug Woo",
209+
id: "debug.woocommerce.ecommerce.monthly",
210+
displayPrice: "$69.99")))
211+
}
212+
}
213+
214+
#endif
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import UIKit
2+
3+
/// View model for `StoreCreationPlanView`.
4+
struct StoreCreationPlanViewModel {
5+
/// Describes a feature for the WPCOM plan with an icon.
6+
struct Feature {
7+
let icon: UIImage
8+
let title: String
9+
}
10+
11+
/// The WPCOM plan to purchase.
12+
let plan: WPComPlanProduct
13+
14+
/// A list of features included in the WPCOM plan.
15+
let features: [Feature] = [
16+
.init(icon: .gridicon(.starOutline), title: Localization.themeFeature),
17+
.init(icon: .gridicon(.product), title: Localization.productsFeature),
18+
.init(icon: .gridicon(.gift), title: Localization.subscriptionsFeature),
19+
.init(icon: .gridicon(.statsAlt), title: Localization.reportFeature),
20+
// TODO: 8108 - update icon
21+
.init(icon: .gridicon(.money), title: Localization.paymentOptionsFeature),
22+
.init(icon: .gridicon(.shipping), title: Localization.shippingLabelsFeature),
23+
.init(icon: .megaphoneIcon, title: Localization.salesChannelsFeature),
24+
]
25+
}
26+
27+
private extension StoreCreationPlanViewModel {
28+
enum Localization {
29+
static let themeFeature = NSLocalizedString(
30+
"Premium themes",
31+
comment: "Title of eCommerce plan feature on the store creation plan screen."
32+
)
33+
static let productsFeature = NSLocalizedString(
34+
"Unlimited products",
35+
comment: "Title of eCommerce plan feature on the store creation plan screen."
36+
)
37+
static let subscriptionsFeature = NSLocalizedString(
38+
"Subscriptions & giftcards",
39+
comment: "Title of eCommerce plan feature on the store creation plan screen."
40+
)
41+
static let reportFeature = NSLocalizedString(
42+
"Ecommerce reports",
43+
comment: "Title of eCommerce plan feature on the store creation plan screen."
44+
)
45+
static let paymentOptionsFeature = NSLocalizedString(
46+
"Multiple payment options",
47+
comment: "Title of eCommerce plan feature on the store creation plan screen."
48+
)
49+
static let shippingLabelsFeature = NSLocalizedString(
50+
"Shipping labels",
51+
comment: "Title of eCommerce plan feature on the store creation plan screen."
52+
)
53+
static let salesChannelsFeature = NSLocalizedString(
54+
"Sales channels",
55+
comment: "Title of eCommerce plan feature on the store creation plan screen."
56+
)
57+
}
58+
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
02077F72253816FF005A78EF /* ProductFormActionsFactory+ReadonlyProductTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02077F71253816FF005A78EF /* ProductFormActionsFactory+ReadonlyProductTests.swift */; };
3131
020886572499E643001D784E /* ProductExternalLinkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020886562499E642001D784E /* ProductExternalLinkViewController.swift */; };
3232
020A55F127F6C605007843F0 /* CardReaderConnectionAnalyticsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020A55F027F6C605007843F0 /* CardReaderConnectionAnalyticsTracker.swift */; };
33+
020AF66329235860007760E5 /* StoreCreationPlanViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020AF66229235860007760E5 /* StoreCreationPlanViewModel.swift */; };
3334
020B2F8F23BD9F1F00BD79AD /* IntegerInputFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020B2F8E23BD9F1F00BD79AD /* IntegerInputFormatter.swift */; };
3435
020B2F9123BDD71500BD79AD /* IntegerInputFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020B2F9023BDD71500BD79AD /* IntegerInputFormatterTests.swift */; };
3536
020B2F9423BDDBDC00BD79AD /* ProductUpdateError+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020B2F9323BDDBDC00BD79AD /* ProductUpdateError+UI.swift */; };
@@ -427,6 +428,7 @@
427428
02ECD1E124FF496200735BE5 /* PaginationTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02ECD1E024FF496200735BE5 /* PaginationTrackerTests.swift */; };
428429
02ECD1E424FF5E0B00735BE5 /* AddProductCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02ECD1E324FF5E0B00735BE5 /* AddProductCoordinator.swift */; };
429430
02ECD1E624FFB4E900735BE5 /* ProductFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02ECD1E524FFB4E900735BE5 /* ProductFactory.swift */; };
431+
02EEA9282923338100D05F47 /* StoreCreationPlanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EEA9272923338100D05F47 /* StoreCreationPlanView.swift */; };
430432
02EEB5C42424AFAA00B8A701 /* TextFieldTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EEB5C22424AFAA00B8A701 /* TextFieldTableViewCell.swift */; };
431433
02EEB5C52424AFAA00B8A701 /* TextFieldTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 02EEB5C32424AFAA00B8A701 /* TextFieldTableViewCell.xib */; };
432434
02F49ADA23BF356E00FA0BFA /* TitleAndTextFieldTableViewCell.ViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F49AD923BF356E00FA0BFA /* TitleAndTextFieldTableViewCell.ViewModel+State.swift */; };
@@ -1991,6 +1993,7 @@
19911993
02077F71253816FF005A78EF /* ProductFormActionsFactory+ReadonlyProductTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProductFormActionsFactory+ReadonlyProductTests.swift"; sourceTree = "<group>"; };
19921994
020886562499E642001D784E /* ProductExternalLinkViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductExternalLinkViewController.swift; sourceTree = "<group>"; };
19931995
020A55F027F6C605007843F0 /* CardReaderConnectionAnalyticsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderConnectionAnalyticsTracker.swift; sourceTree = "<group>"; };
1996+
020AF66229235860007760E5 /* StoreCreationPlanViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreCreationPlanViewModel.swift; sourceTree = "<group>"; };
19941997
020B2F8E23BD9F1F00BD79AD /* IntegerInputFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerInputFormatter.swift; sourceTree = "<group>"; };
19951998
020B2F9023BDD71500BD79AD /* IntegerInputFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerInputFormatterTests.swift; sourceTree = "<group>"; };
19961999
020B2F9323BDDBDC00BD79AD /* ProductUpdateError+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProductUpdateError+UI.swift"; sourceTree = "<group>"; };
@@ -2388,6 +2391,7 @@
23882391
02ECD1E024FF496200735BE5 /* PaginationTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationTrackerTests.swift; sourceTree = "<group>"; };
23892392
02ECD1E324FF5E0B00735BE5 /* AddProductCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProductCoordinator.swift; sourceTree = "<group>"; };
23902393
02ECD1E524FFB4E900735BE5 /* ProductFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductFactory.swift; sourceTree = "<group>"; };
2394+
02EEA9272923338100D05F47 /* StoreCreationPlanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreCreationPlanView.swift; sourceTree = "<group>"; };
23912395
02EEB5C22424AFAA00B8A701 /* TextFieldTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldTableViewCell.swift; sourceTree = "<group>"; };
23922396
02EEB5C32424AFAA00B8A701 /* TextFieldTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TextFieldTableViewCell.xib; sourceTree = "<group>"; };
23932397
02F49AD923BF356E00FA0BFA /* TitleAndTextFieldTableViewCell.ViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TitleAndTextFieldTableViewCell.ViewModel+State.swift"; sourceTree = "<group>"; };
@@ -4527,6 +4531,7 @@
45274531
02759B8F28FFA06F00918176 /* Store Creation */ = {
45284532
isa = PBXGroup;
45294533
children = (
4534+
02EEA92929233F0F00D05F47 /* Plan */,
45304535
02759B9028FFA09600918176 /* StoreCreationWebViewModel.swift */,
45314536
02E3B63029066858007E0F13 /* StoreCreationCoordinator.swift */,
45324537
02EAA4C7290F992B00918DAB /* LoggedOutStoreCreationCoordinator.swift */,
@@ -4939,6 +4944,15 @@
49394944
path = "Add Product";
49404945
sourceTree = "<group>";
49414946
};
4947+
02EEA92929233F0F00D05F47 /* Plan */ = {
4948+
isa = PBXGroup;
4949+
children = (
4950+
02EEA9272923338100D05F47 /* StoreCreationPlanView.swift */,
4951+
020AF66229235860007760E5 /* StoreCreationPlanViewModel.swift */,
4952+
);
4953+
path = Plan;
4954+
sourceTree = "<group>";
4955+
};
49424956
02F4F50C237AFBB700E13A9C /* Cells */ = {
49434957
isa = PBXGroup;
49444958
children = (
@@ -10125,6 +10139,7 @@
1012510139
0245465B24EE7637004F531C /* ProductFormEventLoggerProtocol.swift in Sources */,
1012610140
02C1853D27FF153A00ABD764 /* CardPresentRefundOrchestrator.swift in Sources */,
1012710141
AEC95D432774D07B001571F5 /* CreateOrderAddressFormViewModel.swift in Sources */,
10142+
020AF66329235860007760E5 /* StoreCreationPlanViewModel.swift in Sources */,
1012810143
024DF31B23742E1C006658FE /* FormatBarItemViewProperties.swift in Sources */,
1012910144
26B119BB24D0B62E00FED5C7 /* SurveyViewController.swift in Sources */,
1013010145
CCD2F51A26D67BB50010E679 /* ShippingLabelServicePackageList.swift in Sources */,
@@ -10316,6 +10331,7 @@
1031610331
DE4B3B5826A7041800EEF2D8 /* EdgeInsets+Woo.swift in Sources */,
1031710332
02CE4304276993DA0006EAEF /* CaptureDevicePermissionChecker.swift in Sources */,
1031810333
74A33D8021C3F234009E25DE /* LicensesViewController.swift in Sources */,
10334+
02EEA9282923338100D05F47 /* StoreCreationPlanView.swift in Sources */,
1031910335
454453CA27566CDE00464AC5 /* HubMenuViewModel.swift in Sources */,
1032010336
934CB123224EAB150005CCB9 /* main.swift in Sources */,
1032110337
456396AE25C81D81001F1A26 /* ShippingLabelFormViewModel.swift in Sources */,

0 commit comments

Comments
 (0)