Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ import SwiftUI
PlansTracker.trackPurchaseResult(source: "plan_selection")
}

let domainAddedToCart = plansFlowAfterDomainAddedToCartBlock(customTitle: nil, purchaseCallback: purchaseCallback)

coordinator.domainAddedToCartAndLinkedToSiteCallback = domainAddedToCart

let navigationController = UINavigationController(rootViewController: domainSuggestionsViewController)
dashboardViewController.present(navigationController, animated: true)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,26 @@ class RegisterDomainCoordinator {
typealias DomainPurchasedCallback = ((UIViewController, String) -> Void)
typealias DomainAddedToCartCallback = ((UIViewController, String, Blog) -> Void)

// MARK: Variables
// MARK: Dependencies

private let domainRegistrationService: RegisterDomainDetailsServiceProxyProtocol
private let crashLogger: CrashLogging

// MARK: Variables

let analyticsSource: String

// TODO: This can cause Core Data crashes. Pass `NSManagedObjectID` instead.
var site: Blog?

@salimbraksa salimbraksa Jan 17, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

FYI @hassaanelgarem I'll address this in a separate PR.

var domainPurchasedCallback: DomainPurchasedCallback?
var domainAddedToCartAndLinkedToSiteCallback: DomainAddedToCartCallback?

var domain: DomainSuggestion?

var domainPurchasedCallback: DomainPurchasedCallback?

private var webViewURLChangeObservation: NSKeyValueObservation?

// MARK: - Init

/// Initializes a `RegisterDomainCoordinator` with the specified parameters.
///
/// - Parameters:
Expand All @@ -36,20 +43,23 @@ class RegisterDomainCoordinator {
init(site: Blog?,
domainPurchasedCallback: RegisterDomainCoordinator.DomainPurchasedCallback? = nil,
analyticsSource: String = "domains_register",
crashLogger: CrashLogging = .main) {
crashLogger: CrashLogging = .main,
domainRegistrationService: RegisterDomainDetailsServiceProxyProtocol = RegisterDomainDetailsServiceProxy()) {
self.site = site
self.domainPurchasedCallback = domainPurchasedCallback
self.crashLogger = crashLogger
self.analyticsSource = analyticsSource
self.domainRegistrationService = domainRegistrationService

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This service was instantiated within the createCart method, but I've moved to init for better dependency injection, allowing easier unit testing.

}

// MARK: Public Functions
// MARK: - Public Functions

/// Adds the selected domain to the cart then launches the checkout webview.
/// This flow support purchasing domains only, without plans.
func handlePurchaseDomainOnly(on viewController: UIViewController,
onSuccess: @escaping () -> (),
onFailure: @escaping () -> ()) {
// TODO: Refactor `handlePurchaseDomainOnly` to use `purchaseDomain` helper.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will be addressed in another PR.

createCart { [weak self] result in
switch result {
case .success:
Expand All @@ -66,13 +76,14 @@ class RegisterDomainCoordinator {
func addDomainToCartLinkedToCurrentSite(on viewController: UIViewController,
onSuccess: @escaping () -> (),
onFailure: @escaping () -> ()) {
// TODO: Refactor `addDomainToCartLinkedToCurrentSite` to use `purchaseDomain` helper.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will be addressed in another PR.

guard let blog = site else {
return
}
createCart { [weak self] result in
switch result {
case .success(let domain):
self?.domainAddedToCartAndLinkedToSiteCallback?(viewController, domain.domainName, blog)
self?.presentPlansWebview(for: blog, domain: domain, on: viewController)
onSuccess()
case .failure:
onFailure()
Expand All @@ -83,18 +94,9 @@ class RegisterDomainCoordinator {
/// Related to the `purchaseFromDomainManagement` Domain selection type.
/// Adds the selected domain to the cart then launches the checkout webview
/// The checkout webview is configured for the domain management flow
func handleNoSiteChoice(on viewController: UIViewController,
choicesViewModel: DomainPurchaseChoicesViewModel?) {
createCart { [weak self] result in
switch result {
case .success:
self?.presentCheckoutWebview(on: viewController, title: TextContent.checkoutTitle)
choicesViewModel?.isGetDomainLoading = false

case .failure:
viewController.displayActionableNotice(title: TextContent.errorTitle, actionTitle: TextContent.errorDismiss)
choicesViewModel?.isGetDomainLoading = false
}
func handleNoSiteChoice(on viewController: UIViewController, choicesViewModel: DomainPurchaseChoicesViewModel?) {
self.purchaseDomain(for: site, on: viewController) { _ in
choicesViewModel?.isGetDomainLoading = false
}
}

Expand All @@ -117,18 +119,11 @@ class RegisterDomainCoordinator {
return
}
controller.showLoading()
self.createCart { [weak self] result in
guard let self else {
return
}
switch result {
case .success(let domain):
self.purchaseDomain(for: selectedBlog, on: controller) { result in
controller.hideLoading()
if case .success = result {
self.site = selectedBlog
self.domainAddedToCartAndLinkedToSiteCallback?(controller, domain.domainName, selectedBlog)
case .failure:
controller.displayActionableNotice(title: TextContent.errorTitle, actionTitle: TextContent.errorDismiss)
}
controller.hideLoading()
}
}

Expand All @@ -141,24 +136,66 @@ class RegisterDomainCoordinator {

// MARK: Helpers

/// Initiates the process of purchasing a domain with or without a site.
///
/// - Parameters:
/// - site: The blog site for which the domain is being purchased. Optional.
/// - viewController: The UIViewController from which the domain registration or checkout process is presented.
/// - completion: A closure that is called upon the completion of the purchase attempt.
/// It returns a `Result` indicating either success or failure.
///
/// The method follows this logic:
/// - The "Register Domain" screen is presented when `site.canRegisterDomainWithPaidPlan` is `true`.
/// - If the site does not have any domains (`!site.hasDomains`), the Plans web view is shown.
/// - For sites with existing domains, the Checkout web view is presented,
private func purchaseDomain(for site: Blog?, on viewController: UIViewController, completion: @escaping (Result<Void, Swift.Error>) -> Void) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think a VM would be a better fit for these service calls but it doesn't have to be part of this PR.

guard let domain else {
completion(.failure(Error.noDomainWhenCreatingCart))
return
}
if let site, site.canRegisterDomainWithPaidPlan {
self.presentDomainRegistration(for: site, domain: domain, on: viewController)
completion(.success(()))
return
}
self.createCart { [weak self] result in
guard let self else {
return
}
switch result {
case .success(let domain):
if let site, !site.hasDomains {
self.presentPlansWebview(for: site, domain: domain, on: viewController)
} else {
self.presentCheckoutWebview(on: viewController, title: TextContent.checkoutTitle)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is exactly the change that fixes the issue:

  1. If site.canRegisterDomainWithPaidPlan is true, then we show the Plans web view.
  2. Else, the Checkout web view is shown.

cc @hassaanelgarem

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for clarifying 🙏

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@staskus Do you confirm this logic is correct? 🙏

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I haven't checked the rest of the PR but if site.canRegisterDomainWithPaidPlan it means that the site has a domain credit, therefore we just allow to make domain registration without any plan selection or checkout:

image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@staskus Re-reading SiteDomainsViewModel and SiteDomainsView logic, I understand that:

  • The Register domain screen is shown when site.canRegisterDomainWithPaidPlan is true.
  • The Plans web view is shown when site.canRegisterDomainWithPaidPlan is false and user doesn't have domains.
  • The checkout web view is shown when site.canRegisterDomainWithPaidPlan is false and user has domains.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, sounds right!

completion(.success(()))
case .failure(let error):
viewController.displayActionableNotice(title: TextContent.errorTitle, actionTitle: TextContent.errorDismiss)
Comment thread
salimbraksa marked this conversation as resolved.
completion(.failure(error))
}
}
}

private func createCart(completion: @escaping (Result<DomainSuggestion, Swift.Error>) -> Void) {
guard let domain else {
completion(.failure(Error.noDomainWhenCreatingCart))
return
}
let siteID = site?.dotComID?.intValue
let proxy = RegisterDomainDetailsServiceProxy()
proxy.createPersistentDomainShoppingCart(siteID: siteID,
domainSuggestion: domain,
privacyProtectionEnabled: domain.supportsPrivacy ?? false,
success: { _ in
self.domainRegistrationService.createPersistentDomainShoppingCart(siteID: siteID,
domainSuggestion: domain,
privacyProtectionEnabled: domain.supportsPrivacy ?? false,
success: { _ in
completion(.success(domain))
},
failure: { error in
failure: { error in
completion(.failure(error))
})
}

// MARK: - Presenting Checkout Web View

private func presentCheckoutWebview(on viewController: UIViewController,
title: String?) {
guard let domain,
Expand Down Expand Up @@ -252,6 +289,46 @@ class RegisterDomainCoordinator {
}
}

// MARK: - Presenting Domain Registration View

private func presentDomainRegistration(
for site: Blog,
domain: DomainSuggestion,
on viewController: UIViewController
) {
guard let siteID = site.dotComID?.intValue else {
return
}
let destination = RegisterDomainDetailsViewController()
destination.viewModel = RegisterDomainDetailsViewModel(siteID: siteID, domain: domain) { [weak self] name in
guard let self = self else {
return
}
self.domainPurchasedCallback?(viewController, name)
self.trackDomainPurchasingCompleted()
}
}

// MARK: - Presenting Plans

private func presentPlansWebview(
for site: Blog,
domain: DomainSuggestion,
on viewController: UIViewController
) {
let presentPlansFlow = FreeToPaidPlansCoordinator.plansFlowAfterDomainAddedToCartBlock(
customTitle: nil,
analyticsSource: analyticsSource
) { [weak self] controller, domain in
guard let self else {
return
}
self.domainPurchasedCallback?(controller, domain)
self.trackDomainPurchasingCompleted()
}
presentPlansFlow(viewController, domain.domainName, site)
}

// MARK: - Tracks

private func track(_ event: WPAnalyticsEvent, properties: [AnyHashable: Any]? = nil) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ struct DomainsDashboardFactory {
}

static func makeDomainsSuggestionViewController(blog: Blog, domainSelectionType: DomainSelectionType, onDismiss: @escaping () -> Void) -> DomainSelectionViewController {
let coordinator = RegisterDomainCoordinator(site: blog)
let coordinator = RegisterDomainCoordinator(site: blog, analyticsSource: "site_domains")
let viewController = DomainSelectionViewController(
service: DomainsServiceAdapter(coreDataStack: ContextManager.shared),
domainSelectionType: domainSelectionType,
Expand All @@ -29,16 +29,6 @@ struct DomainsDashboardFactory {
viewController.present(controller, animated: true)
}

let domainAddedToCart = FreeToPaidPlansCoordinator.plansFlowAfterDomainAddedToCartBlock(
customTitle: nil,
analyticsSource: "site_domains"
) { [weak coordinator] controller, domain in
coordinator?.domainPurchasedCallback?(controller, domain)
coordinator?.trackDomainPurchasingCompleted()
}

coordinator.domainAddedToCartAndLinkedToSiteCallback = domainAddedToCart

return viewController
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,7 @@ import Foundation
}
}

let domainAddedToCart = FreeToPaidPlansCoordinator.plansFlowAfterDomainAddedToCartBlock(
customTitle: nil,
analyticsSource: analyticsSource
) { [weak coordinator] controller, domain in
domainPurchasedCallback(controller, domain)
coordinator?.trackDomainPurchasingCompleted()
}

coordinator.domainPurchasedCallback = domainPurchasedCallback // For no site flow (domain only)
coordinator.domainAddedToCartAndLinkedToSiteCallback = domainAddedToCart // For existing site flow (plans)

let navigationController = UINavigationController(rootViewController: domainSuggestionsViewController)
navigationController.isModalInPresentation = true
Expand Down