Skip to content

Commit 8af7cdd

Browse files
authored
Merge pull request #8109 from woocommerce/feat/8106-store-creation-summary
Store creation M2: show store summary after store creation success
2 parents 43203f6 + b0b8bf9 commit 8af7cdd

File tree

8 files changed

+271
-10
lines changed

8 files changed

+271
-10
lines changed

WooCommerce/Classes/Authentication/Store Creation/StoreCreationCoordinator.swift

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ final class StoreCreationCoordinator: Coordinator {
1919
@Published private var possibleSiteURLsFromStoreCreation: Set<String> = []
2020
private var possibleSiteURLsFromStoreCreationSubscription: AnyCancellable?
2121

22+
private let stores: StoresManager
2223
private let analytics: Analytics
2324
private let source: Source
2425
private let storePickerViewModel: StorePickerViewModel
@@ -39,6 +40,7 @@ final class StoreCreationCoordinator: Coordinator {
3940
storageManager: storageManager,
4041
analytics: analytics)
4142
self.switchStoreUseCase = SwitchStoreUseCase(stores: stores, storageManager: storageManager)
43+
self.stores = stores
4244
self.analytics = analytics
4345
self.featureFlagService = featureFlagService
4446
}
@@ -67,14 +69,18 @@ private extension StoreCreationCoordinator {
6769
}
6870

6971
func startStoreCreationM2() {
72+
let storeCreationNavigationController = UINavigationController()
73+
storeCreationNavigationController.navigationBar.prefersLargeTitles = true
74+
7075
let domainSelector = DomainSelectorHostingController(viewModel: .init(),
71-
onDomainSelection: { domain in
72-
// TODO-8045: navigate to the next step of store creation.
76+
onDomainSelection: { [weak self] domain in
77+
guard let self else { return }
78+
// TODO: add a store name screen before the domain selector screen.
79+
await self.createStoreAndContinueToStoreSummary(from: storeCreationNavigationController, name: "Test store", domain: domain)
7380
}, onSkip: {
7481
// TODO-8045: skip to the next step of store creation with an auto-generated domain.
7582
})
76-
let storeCreationNavigationController = UINavigationController(rootViewController: domainSelector)
77-
storeCreationNavigationController.navigationBar.prefersLargeTitles = true
83+
storeCreationNavigationController.pushViewController(domainSelector, animated: false)
7884
presentStoreCreation(viewController: storeCreationNavigationController)
7985
}
8086

@@ -91,6 +97,8 @@ private extension StoreCreationCoordinator {
9197
}
9298
}
9399

100+
// MARK: - Store creation M1
101+
94102
private extension StoreCreationCoordinator {
95103
func observeSiteURLsFromStoreCreation() {
96104
possibleSiteURLsFromStoreCreationSubscription = $possibleSiteURLsFromStoreCreation
@@ -181,6 +189,57 @@ private extension StoreCreationCoordinator {
181189
}
182190
}
183191

192+
// MARK: - Store creation M2
193+
194+
private extension StoreCreationCoordinator {
195+
@MainActor
196+
func createStoreAndContinueToStoreSummary(from navigationController: UINavigationController, name: String, domain: String) async {
197+
let result = await createStore(name: name, domain: domain)
198+
switch result {
199+
case .success(let siteResult):
200+
showStoreSummary(from: navigationController, result: siteResult)
201+
case .failure(let error):
202+
showStoreCreationErrorAlert(from: navigationController, error: error)
203+
}
204+
}
205+
206+
@MainActor
207+
func createStore(name: String, domain: String) async -> Result<SiteCreationResult, SiteCreationError> {
208+
await withCheckedContinuation { continuation in
209+
stores.dispatch(SiteAction.createSite(name: name, domain: domain) { result in
210+
continuation.resume(returning: result)
211+
})
212+
}
213+
}
214+
215+
@MainActor
216+
func showStoreSummary(from navigationController: UINavigationController, result: SiteCreationResult) {
217+
let viewModel = StoreCreationSummaryViewModel(storeName: result.name, storeSlug: result.siteSlug)
218+
let storeSummary = StoreCreationSummaryHostingController(viewModel: viewModel) {
219+
// TODO: 8108 - integrate IAP.
220+
}
221+
navigationController.pushViewController(storeSummary, animated: true)
222+
}
223+
224+
@MainActor
225+
func showStoreCreationErrorAlert(from navigationController: UINavigationController, error: SiteCreationError) {
226+
let message: String = {
227+
switch error {
228+
case .invalidDomain, .domainExists:
229+
return Localization.StoreCreationErrorAlert.domainErrorMessage
230+
default:
231+
return Localization.StoreCreationErrorAlert.defaultErrorMessage
232+
}
233+
}()
234+
let alertController = UIAlertController(title: Localization.StoreCreationErrorAlert.title,
235+
message: message,
236+
preferredStyle: .alert)
237+
alertController.view.tintColor = .text
238+
_ = alertController.addCancelActionWithTitle(Localization.StoreCreationErrorAlert.cancelActionTitle) { _ in }
239+
navigationController.present(alertController, animated: true)
240+
}
241+
}
242+
184243
private extension StoreCreationCoordinator {
185244
enum StoreCreationCoordinatorError: Error {
186245
case selfDeallocated
@@ -197,6 +256,19 @@ private extension StoreCreationCoordinator {
197256
static let cancelActionTitle = NSLocalizedString("Cancel",
198257
comment: "Button title Cancel in Discard Changes Action Sheet")
199258
}
259+
260+
enum StoreCreationErrorAlert {
261+
static let title = NSLocalizedString("Cannot create store",
262+
comment: "Title of the alert when the store cannot be created in the store creation flow.")
263+
static let domainErrorMessage = NSLocalizedString("Please try a different domain.",
264+
comment: "Message of the alert when the store cannot be created due to the domain in the store creation flow.")
265+
static let defaultErrorMessage = NSLocalizedString("Please try again.",
266+
comment: "Message of the alert when the store cannot be created in the store creation flow.")
267+
static let cancelActionTitle = NSLocalizedString(
268+
"OK",
269+
comment: "Button title to dismiss the alert when the store cannot be created in the store creation flow."
270+
)
271+
}
200272
}
201273
}
202274

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import SwiftUI
2+
3+
/// Hosting controller that wraps the `StoreCreationSummaryView`.
4+
final class StoreCreationSummaryHostingController: UIHostingController<StoreCreationSummaryView> {
5+
private let onContinueToPayment: () -> Void
6+
7+
init(viewModel: StoreCreationSummaryViewModel,
8+
onContinueToPayment: @escaping () -> Void) {
9+
self.onContinueToPayment = onContinueToPayment
10+
super.init(rootView: StoreCreationSummaryView(viewModel: viewModel))
11+
12+
rootView.onContinueToPayment = { [weak self] in
13+
self?.onContinueToPayment()
14+
}
15+
}
16+
17+
@available(*, unavailable)
18+
required dynamic init?(coder aDecoder: NSCoder) {
19+
fatalError("init(coder:) has not been implemented")
20+
}
21+
22+
override func viewDidLoad() {
23+
super.viewDidLoad()
24+
25+
configureNavigationBarAppearance()
26+
}
27+
28+
/// Shows a transparent navigation bar without a bottom border.
29+
func configureNavigationBarAppearance() {
30+
navigationItem.title = Localization.title
31+
32+
let appearance = UINavigationBarAppearance()
33+
appearance.configureWithTransparentBackground()
34+
appearance.backgroundColor = .systemBackground
35+
36+
navigationItem.standardAppearance = appearance
37+
navigationItem.scrollEdgeAppearance = appearance
38+
navigationItem.compactAppearance = appearance
39+
}
40+
}
41+
42+
private extension StoreCreationSummaryHostingController {
43+
enum Localization {
44+
static let title = NSLocalizedString("My store", comment: "Title of the store creation summary screen.")
45+
}
46+
}
47+
48+
/// View model for `StoreCreationSummaryView`.
49+
struct StoreCreationSummaryViewModel {
50+
/// The name of the store.
51+
let storeName: String
52+
/// The URL slug of the store.
53+
let storeSlug: String
54+
}
55+
56+
/// Displays a summary of the store creation flow with the store information (e.g. store name, store slug).
57+
struct StoreCreationSummaryView: View {
58+
/// Set in the hosting controller.
59+
var onContinueToPayment: (() -> Void) = {}
60+
61+
private let viewModel: StoreCreationSummaryViewModel
62+
63+
init(viewModel: StoreCreationSummaryViewModel) {
64+
self.viewModel = viewModel
65+
}
66+
67+
var body: some View {
68+
VStack(alignment: .leading, spacing: 0) {
69+
ScrollView {
70+
VStack(alignment: .leading, spacing: Layout.spacingBetweenSubtitleAndStoreInfo) {
71+
// Header label.
72+
Text(Localization.subtitle)
73+
.foregroundColor(Color(.secondaryLabel))
74+
.bodyStyle()
75+
76+
// Store info.
77+
VStack(alignment: .leading, spacing: 0) {
78+
// Image.
79+
HStack {
80+
Spacer()
81+
Image(uiImage: .storeSummaryImage)
82+
Spacer()
83+
}
84+
.background(Color(.systemColor(.systemGray6)))
85+
86+
VStack {
87+
VStack(alignment: .leading, spacing: Layout.spacingBetweenStoreNameAndDomain) {
88+
// Store name.
89+
Text(viewModel.storeName)
90+
.headlineStyle()
91+
// Store URL slug.
92+
Text(viewModel.storeSlug)
93+
.foregroundColor(Color(.secondaryLabel))
94+
.bodyStyle()
95+
}
96+
}
97+
.padding(Layout.storeInfoPadding)
98+
}
99+
.cornerRadius(Layout.storeInfoCornerRadius)
100+
.overlay(
101+
RoundedRectangle(cornerRadius: Layout.storeInfoCornerRadius)
102+
.stroke(Color(.separator), lineWidth: 0.5)
103+
)
104+
}
105+
.padding(Layout.defaultPadding)
106+
}
107+
108+
// Continue button.
109+
Group {
110+
Divider()
111+
.frame(height: Layout.dividerHeight)
112+
.foregroundColor(Color(.separator))
113+
Button(Localization.continueButtonTitle) {
114+
onContinueToPayment()
115+
}
116+
.buttonStyle(PrimaryButtonStyle())
117+
.padding(Layout.defaultButtonPadding)
118+
}
119+
}
120+
}
121+
}
122+
123+
private extension StoreCreationSummaryView {
124+
enum Layout {
125+
static let spacingBetweenSubtitleAndStoreInfo: CGFloat = 40
126+
static let spacingBetweenStoreNameAndDomain: CGFloat = 4
127+
static let defaultHorizontalPadding: CGFloat = 16
128+
static let dividerHeight: CGFloat = 1
129+
static let defaultPadding: EdgeInsets = .init(top: 16, leading: 16, bottom: 16, trailing: 16)
130+
static let defaultButtonPadding: EdgeInsets = .init(top: 10, leading: 16, bottom: 10, trailing: 16)
131+
static let storeInfoPadding: EdgeInsets = .init(top: 16, leading: 16, bottom: 16, trailing: 16)
132+
static let storeInfoCornerRadius: CGFloat = 8
133+
}
134+
135+
enum Localization {
136+
static let subtitle = NSLocalizedString(
137+
"Your store will be created based on the options of your choice!",
138+
comment: "Subtitle of the store creation summary screen.")
139+
static let continueButtonTitle = NSLocalizedString(
140+
"Continue to Payment",
141+
comment: "Title of the button on the store creation summary view to continue to payment."
142+
)
143+
}
144+
}
145+
146+
struct StoreCreationSummaryView_Previews: PreviewProvider {
147+
static var previews: some View {
148+
StoreCreationSummaryView(viewModel:
149+
.init(storeName: "Fruity shop", storeSlug: "fruityshop.com"))
150+
StoreCreationSummaryView(viewModel:
151+
.init(storeName: "Fruity shop", storeSlug: "fruityshop.com"))
152+
.preferredColorScheme(.dark)
153+
}
154+
}

WooCommerce/Classes/Extensions/UIImage+Woo.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,12 @@ extension UIImage {
373373
UIImage(named: "icon-store")!
374374
}
375375

376+
/// Store summary image used in the store creation flow.
377+
///
378+
static var storeSummaryImage: UIImage {
379+
return UIImage(named: "store-summary")!
380+
}
381+
376382
/// Cog Image
377383
///
378384
static var cogImage: UIImage {

WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,23 @@ import SwiftUI
33
/// Hosting controller that wraps the `DomainSelectorView` view.
44
final class DomainSelectorHostingController: UIHostingController<DomainSelectorView> {
55
private let viewModel: DomainSelectorViewModel
6-
private let onDomainSelection: (String) -> Void
6+
private let onDomainSelection: (String) async -> Void
77
private let onSkip: () -> Void
88

99
/// - Parameters:
1010
/// - viewModel: View model for the domain selector.
1111
/// - onDomainSelection: Called when the user continues with a selected domain name.
1212
/// - onSkip: Called when the user taps to skip domain selection.
1313
init(viewModel: DomainSelectorViewModel,
14-
onDomainSelection: @escaping (String) -> Void,
14+
onDomainSelection: @escaping (String) async -> Void,
1515
onSkip: @escaping () -> Void) {
1616
self.viewModel = viewModel
1717
self.onDomainSelection = onDomainSelection
1818
self.onSkip = onSkip
1919
super.init(rootView: DomainSelectorView(viewModel: viewModel))
2020

2121
rootView.onDomainSelection = { [weak self] domain in
22-
self?.onDomainSelection(domain)
22+
await self?.onDomainSelection(domain)
2323
}
2424
}
2525

@@ -70,7 +70,7 @@ private extension DomainSelectorHostingController {
7070
/// Allows the user to search for a domain and then select one to continue.
7171
struct DomainSelectorView: View {
7272
/// Set in the hosting controller.
73-
var onDomainSelection: ((String) -> Void) = { _ in }
73+
var onDomainSelection: ((String) async -> Void) = { _ in }
7474

7575
/// View model to drive the view.
7676
@ObservedObject private var viewModel: DomainSelectorViewModel
@@ -80,6 +80,8 @@ struct DomainSelectorView: View {
8080
/// when a domain row is selected.
8181
@State private var selectedDomainName: String?
8282

83+
@State private var isWaitingForDomainSelectionCompletion: Bool = false
84+
8385
init(viewModel: DomainSelectorViewModel) {
8486
self.viewModel = viewModel
8587
}
@@ -154,9 +156,13 @@ struct DomainSelectorView: View {
154156
.frame(height: Layout.dividerHeight)
155157
.foregroundColor(Color(.separator))
156158
Button(Localization.continueButtonTitle) {
157-
onDomainSelection(selectedDomainName)
159+
Task { @MainActor in
160+
isWaitingForDomainSelectionCompletion = true
161+
await onDomainSelection(selectedDomainName)
162+
isWaitingForDomainSelectionCompletion = false
163+
}
158164
}
159-
.buttonStyle(PrimaryButtonStyle())
165+
.buttonStyle(PrimaryLoadingButtonStyle(isLoading: isWaitingForDomainSelectionCompletion))
160166
.padding(Layout.defaultPadding)
161167
}
162168
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "store-summary.pdf",
5+
"idiom" : "universal"
6+
}
7+
],
8+
"info" : {
9+
"author" : "xcode",
10+
"version" : 1
11+
},
12+
"properties" : {
13+
"preserves-vector-representation" : true
14+
}
15+
}
Binary file not shown.

0 commit comments

Comments
 (0)