Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ final class StoreCreationCoordinator: Coordinator {
@Published private var possibleSiteURLsFromStoreCreation: Set<String> = []
private var possibleSiteURLsFromStoreCreationSubscription: AnyCancellable?

private let stores: StoresManager
private let analytics: Analytics
private let source: Source
private let storePickerViewModel: StorePickerViewModel
Expand All @@ -39,6 +40,7 @@ final class StoreCreationCoordinator: Coordinator {
storageManager: storageManager,
analytics: analytics)
self.switchStoreUseCase = SwitchStoreUseCase(stores: stores, storageManager: storageManager)
self.stores = stores
self.analytics = analytics
self.featureFlagService = featureFlagService
}
Expand Down Expand Up @@ -67,14 +69,18 @@ private extension StoreCreationCoordinator {
}

func startStoreCreationM2() {
let storeCreationNavigationController = UINavigationController()
storeCreationNavigationController.navigationBar.prefersLargeTitles = true

let domainSelector = DomainSelectorHostingController(viewModel: .init(),
onDomainSelection: { domain in
// TODO-8045: navigate to the next step of store creation.
onDomainSelection: { [weak self] domain in
guard let self else { return }
// TODO: add a store name screen before the domain selector screen.
await self.createStoreAndContinueToStoreSummary(from: storeCreationNavigationController, name: "Test store", domain: domain)
}, onSkip: {
// TODO-8045: skip to the next step of store creation with an auto-generated domain.
})
let storeCreationNavigationController = UINavigationController(rootViewController: domainSelector)
storeCreationNavigationController.navigationBar.prefersLargeTitles = true
storeCreationNavigationController.pushViewController(domainSelector, animated: false)
presentStoreCreation(viewController: storeCreationNavigationController)
}

Expand All @@ -91,6 +97,8 @@ private extension StoreCreationCoordinator {
}
}

// MARK: - Store creation M1

private extension StoreCreationCoordinator {
func observeSiteURLsFromStoreCreation() {
possibleSiteURLsFromStoreCreationSubscription = $possibleSiteURLsFromStoreCreation
Expand Down Expand Up @@ -181,6 +189,57 @@ private extension StoreCreationCoordinator {
}
}

// MARK: - Store creation M2

private extension StoreCreationCoordinator {
@MainActor
func createStoreAndContinueToStoreSummary(from navigationController: UINavigationController, name: String, domain: String) async {
let result = await createStore(name: name, domain: domain)
switch result {
case .success(let siteResult):
showStoreSummary(from: navigationController, result: siteResult)
case .failure(let error):
showStoreCreationErrorAlert(from: navigationController, error: error)
}
}

@MainActor
func createStore(name: String, domain: String) async -> Result<SiteCreationResult, SiteCreationError> {
await withCheckedContinuation { continuation in
stores.dispatch(SiteAction.createSite(name: name, domain: domain) { result in
continuation.resume(returning: result)
})
}
}

@MainActor
func showStoreSummary(from navigationController: UINavigationController, result: SiteCreationResult) {
let viewModel = StoreCreationSummaryViewModel(storeName: result.name, storeSlug: result.siteSlug)
let storeSummary = StoreCreationSummaryHostingController(viewModel: viewModel) {
// TODO: 8108 - integrate IAP.
}
navigationController.pushViewController(storeSummary, animated: true)
}

@MainActor
func showStoreCreationErrorAlert(from navigationController: UINavigationController, error: SiteCreationError) {
let message: String = {
switch error {
case .invalidDomain, .domainExists:
return Localization.StoreCreationErrorAlert.domainErrorMessage
default:
return Localization.StoreCreationErrorAlert.defaultErrorMessage
}
}()
let alertController = UIAlertController(title: Localization.StoreCreationErrorAlert.title,
message: message,
preferredStyle: .alert)
alertController.view.tintColor = .text
_ = alertController.addCancelActionWithTitle(Localization.StoreCreationErrorAlert.cancelActionTitle) { _ in }
navigationController.present(alertController, animated: true)
}
}

private extension StoreCreationCoordinator {
enum StoreCreationCoordinatorError: Error {
case selfDeallocated
Expand All @@ -197,6 +256,19 @@ private extension StoreCreationCoordinator {
static let cancelActionTitle = NSLocalizedString("Cancel",
comment: "Button title Cancel in Discard Changes Action Sheet")
}

enum StoreCreationErrorAlert {
static let title = NSLocalizedString("Cannot create store",
comment: "Title of the alert when the store cannot be created in the store creation flow.")
static let domainErrorMessage = NSLocalizedString("Please try a different domain.",
comment: "Message of the alert when the store cannot be created due to the domain in the store creation flow.")
static let defaultErrorMessage = NSLocalizedString("Please try again.",
comment: "Message of the alert when the store cannot be created in the store creation flow.")
static let cancelActionTitle = NSLocalizedString(
"OK",
comment: "Button title to dismiss the alert when the store cannot be created in the store creation flow."
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import SwiftUI

/// Hosting controller that wraps the `StoreCreationSummaryView`.
final class StoreCreationSummaryHostingController: UIHostingController<StoreCreationSummaryView> {
private let onContinueToPayment: () -> Void

init(viewModel: StoreCreationSummaryViewModel,
onContinueToPayment: @escaping () -> Void) {
self.onContinueToPayment = onContinueToPayment
super.init(rootView: StoreCreationSummaryView(viewModel: viewModel))

rootView.onContinueToPayment = { [weak self] in
self?.onContinueToPayment()
}
}

@available(*, unavailable)
required dynamic init?(coder aDecoder: NSCoder) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we can also add an @available attribute here to signal the compiler in compile-time that this initializer is not ready for use:

Suggested change
required dynamic init?(coder aDecoder: NSCoder) {
@available(*, unavailable)
required dynamic init?(coder aDecoder: NSCoder) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, updated in b0b8bf9

fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()

configureNavigationBarAppearance()
}

/// Shows a transparent navigation bar without a bottom border.
func configureNavigationBarAppearance() {
navigationItem.title = Localization.title

let appearance = UINavigationBarAppearance()
appearance.configureWithTransparentBackground()
appearance.backgroundColor = .systemBackground

navigationItem.standardAppearance = appearance
navigationItem.scrollEdgeAppearance = appearance
navigationItem.compactAppearance = appearance
}
}

private extension StoreCreationSummaryHostingController {
enum Localization {
static let title = NSLocalizedString("My store", comment: "Title of the store creation summary screen.")
}
}

/// View model for `StoreCreationSummaryView`.
struct StoreCreationSummaryViewModel {
/// The name of the store.
let storeName: String
/// The URL slug of the store.
let storeSlug: String
}

/// Displays a summary of the store creation flow with the store information (e.g. store name, store slug).
struct StoreCreationSummaryView: View {
/// Set in the hosting controller.
var onContinueToPayment: (() -> Void) = {}

private let viewModel: StoreCreationSummaryViewModel

init(viewModel: StoreCreationSummaryViewModel) {
self.viewModel = viewModel
}

var body: some View {
VStack(alignment: .leading, spacing: 0) {
ScrollView {
VStack(alignment: .leading, spacing: Layout.spacingBetweenSubtitleAndStoreInfo) {
// Header label.
Text(Localization.subtitle)
.foregroundColor(Color(.secondaryLabel))
.bodyStyle()

// Store info.
VStack(alignment: .leading, spacing: 0) {
// Image.
HStack {
Spacer()
Image(uiImage: .storeSummaryImage)
Spacer()
}
.background(Color(.systemColor(.systemGray6)))

VStack {
VStack(alignment: .leading, spacing: Layout.spacingBetweenStoreNameAndDomain) {
// Store name.
Text(viewModel.storeName)
.headlineStyle()
// Store URL slug.
Text(viewModel.storeSlug)
.foregroundColor(Color(.secondaryLabel))
.bodyStyle()
}
}
.padding(Layout.storeInfoPadding)
}
.cornerRadius(Layout.storeInfoCornerRadius)
.overlay(
RoundedRectangle(cornerRadius: Layout.storeInfoCornerRadius)
.stroke(Color(.separator), lineWidth: 0.5)
)
}
.padding(Layout.defaultPadding)
}

// Continue button.
Group {
Divider()
.frame(height: Layout.dividerHeight)
.foregroundColor(Color(.separator))
Button(Localization.continueButtonTitle) {
onContinueToPayment()
}
.buttonStyle(PrimaryButtonStyle())
.padding(Layout.defaultButtonPadding)
}
}
}
}

private extension StoreCreationSummaryView {
enum Layout {
static let spacingBetweenSubtitleAndStoreInfo: CGFloat = 40
static let spacingBetweenStoreNameAndDomain: CGFloat = 4
static let defaultHorizontalPadding: CGFloat = 16
static let dividerHeight: CGFloat = 1
static let defaultPadding: EdgeInsets = .init(top: 16, leading: 16, bottom: 16, trailing: 16)
static let defaultButtonPadding: EdgeInsets = .init(top: 10, leading: 16, bottom: 10, trailing: 16)
static let storeInfoPadding: EdgeInsets = .init(top: 16, leading: 16, bottom: 16, trailing: 16)
static let storeInfoCornerRadius: CGFloat = 8
}

enum Localization {
static let subtitle = NSLocalizedString(
"Your store will be created based on the options of your choice!",
comment: "Subtitle of the store creation summary screen.")
static let continueButtonTitle = NSLocalizedString(
"Continue to Payment",
comment: "Title of the button on the store creation summary view to continue to payment."
)
}
}

struct StoreCreationSummaryView_Previews: PreviewProvider {
static var previews: some View {
StoreCreationSummaryView(viewModel:
.init(storeName: "Fruity shop", storeSlug: "fruityshop.com"))
StoreCreationSummaryView(viewModel:
.init(storeName: "Fruity shop", storeSlug: "fruityshop.com"))
.preferredColorScheme(.dark)
}
}
6 changes: 6 additions & 0 deletions WooCommerce/Classes/Extensions/UIImage+Woo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,12 @@ extension UIImage {
UIImage(named: "icon-store")!
}

/// Store summary image used in the store creation flow.
///
static var storeSummaryImage: UIImage {
return UIImage(named: "store-summary")!
}

/// Cog Image
///
static var cogImage: UIImage {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@ import SwiftUI
/// Hosting controller that wraps the `DomainSelectorView` view.
final class DomainSelectorHostingController: UIHostingController<DomainSelectorView> {
private let viewModel: DomainSelectorViewModel
private let onDomainSelection: (String) -> Void
private let onDomainSelection: (String) async -> Void
private let onSkip: () -> Void

/// - Parameters:
/// - viewModel: View model for the domain selector.
/// - onDomainSelection: Called when the user continues with a selected domain name.
/// - onSkip: Called when the user taps to skip domain selection.
init(viewModel: DomainSelectorViewModel,
onDomainSelection: @escaping (String) -> Void,
onDomainSelection: @escaping (String) async -> Void,
onSkip: @escaping () -> Void) {
self.viewModel = viewModel
self.onDomainSelection = onDomainSelection
self.onSkip = onSkip
super.init(rootView: DomainSelectorView(viewModel: viewModel))

rootView.onDomainSelection = { [weak self] domain in
self?.onDomainSelection(domain)
await self?.onDomainSelection(domain)
}
}

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

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

@State private var isWaitingForDomainSelectionCompletion: Bool = false

init(viewModel: DomainSelectorViewModel) {
self.viewModel = viewModel
}
Expand Down Expand Up @@ -154,9 +156,13 @@ struct DomainSelectorView: View {
.frame(height: Layout.dividerHeight)
.foregroundColor(Color(.separator))
Button(Localization.continueButtonTitle) {
onDomainSelection(selectedDomainName)
Task { @MainActor in
isWaitingForDomainSelectionCompletion = true
await onDomainSelection(selectedDomainName)
isWaitingForDomainSelectionCompletion = false
}
}
.buttonStyle(PrimaryButtonStyle())
.buttonStyle(PrimaryLoadingButtonStyle(isLoading: isWaitingForDomainSelectionCompletion))
.padding(Layout.defaultPadding)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "store-summary.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
Binary file not shown.
Loading