Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ final class AuthenticatedWebViewController: UIViewController {
viewModel.handleDismissal()
}
}

override func viewDidDisappear(_ animated: Bool) {
viewModel.handleDisappear()
super.viewDidDisappear(animated)
}
}

private extension AuthenticatedWebViewController {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ protocol AuthenticatedWebViewModel {
/// Triggered when the web view is dismissed
func handleDismissal()

/// Triggered when the web view is disappeared
func handleDisappear()

/// Triggered when the web view redirects to a new URL
func handleRedirect(for url: URL?)

Expand Down Expand Up @@ -53,6 +56,10 @@ extension AuthenticatedWebViewModel {
func didFailProvisionalNavigation(with error: Error) {
// NO-OP
}

func handleDisappear () {
// NO-OP
}
}

// MARK: - Helper methods
Expand Down
4 changes: 4 additions & 0 deletions WooCommerce/Classes/Authentication/WPAdminWebViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class WPAdminWebViewModel: AuthenticatedWebViewModel, WebviewReloadable {
// no-op
}

func handleDisappear() {
// no-op
}

func handleRedirect(for url: URL?) {
guard let url else {
return
Expand Down
2 changes: 0 additions & 2 deletions WooCommerce/Classes/CIAB/CIABEligibilityChecker.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/// periphery: ignore:all - Will be used in upcoming PRs

import Foundation
import Yosemite

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Foundation
import Yosemite

/// periphery: ignore - Will be used in upcoming changes for app feature gating
protocol CIABEligibilityCheckerProtocol {
var isCurrentSiteCIAB: Bool { get }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Yosemite
import UIKit

/// Coordinator for the **native** product detail/editor flow.
/// Delegates VC construction to `ProductDetailsFactory` and applies the requested presentation style.
class ProductDetailNativeCoordinator {

func viewController(
product: Product,
presentationStyle: ProductDetailNavigator.Presentation,
isReadOnly: Bool,
onDelete: (() -> Void)? = nil) -> UIViewController {
return ProductDetailsFactory.productDetails(product: product,
presentationStyle: presentationStyle.asProductFormPresentationStyle,
forceReadOnly: isReadOnly,
onDeleteCompletion: onDelete ?? {})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import UIKit
import Yosemite

/// Coordinator for the **admin web** product detail/editor flow.
class ProductDetailWebCoordinator: NSObject {
private let site: Site?

init(site: Site?) {
self.site = site
}

func viewController(product: Product, onDismiss: @escaping () -> Void) -> UIViewController {
guard let url = ProductAdminURLProvider.editURL(for: product, site: site) else {
return UIViewController()
}

let viewModel = AdminWebViewModel(title: product.name, initialURL: url) { [onDismiss] in
onDismiss()
}
let webViewController = AuthenticatedWebViewController(viewModel: viewModel)
return webViewController
}
}

fileprivate class AdminWebViewModel: WPAdminWebViewModel {
let onDismiss: (() -> Void)

init(title: String, initialURL: URL, onDismiss: @escaping () -> Void) {
self.onDismiss = onDismiss
super.init(title: title, initialURL: initialURL)
}

override func handleDisappear() {
onDismiss()
super.handleDisappear()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Yosemite
import Foundation

/// Builds canonical admin edit URLs for products.
enum ProductAdminURLProvider {

static func editURL(for product: Product, site: Site?) -> URL? {
guard let base = site?.adminURLWithFallback() else { return nil }

var components = URLComponents(url: base, resolvingAgainstBaseURL: false)!
components.queryItems = [
URLQueryItem(name: "page", value: "next-admin"),
URLQueryItem(name: "p", value: "/woocommerce/products/edit/\(product.productID)")
]

return components.url
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Yosemite

/// Factory for producing coordinators used by the navigator.
protocol ProductDetailCoordinatorFactoryProtocol {
func webCoordinator(site: Site?) -> ProductDetailWebCoordinator
func nativeCoordinator() -> ProductDetailNativeCoordinator
}

/// Default coordinator factory that wires production dependencies.
class ProductDetailCoordinatorFactory: ProductDetailCoordinatorFactoryProtocol {
static let `default` = ProductDetailCoordinatorFactory()

func webCoordinator(site: Site?) -> ProductDetailWebCoordinator {
return ProductDetailWebCoordinator(site: site)
}

func nativeCoordinator() -> ProductDetailNativeCoordinator {
return ProductDetailNativeCoordinator()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import UIKit
import Yosemite

/// Decides between **native** and **admin web** product detail flows and builds the destination VC.
final class ProductDetailNavigator {
/// Describes how the **native** destination should be presented.
enum Presentation {
case push
case contained(in: () -> UIViewController?)

var asProductFormPresentationStyle: ProductFormPresentationStyle {
switch self {
case .contained(let inVC):
.contained(containerViewController: inVC)
case .push:
.navigationStack
}
}
}

static var shared = ProductDetailNavigator()

private let ciabChecker: CIABEligibilityCheckerProtocol
private let coordinatorFactory: ProductDetailCoordinatorFactoryProtocol
private let stores: StoresManager

init(ciabChecker: CIABEligibilityCheckerProtocol = CIABEligibilityChecker(),
coordinatorFactory: ProductDetailCoordinatorFactoryProtocol = ProductDetailCoordinatorFactory.default,
stores: StoresManager = ServiceLocator.stores,
) {
self.ciabChecker = ciabChecker
self.coordinatorFactory = coordinatorFactory
self.stores = stores
}

/// Builds the destination `UIViewController` for the given product.
/// - Parameters:
/// - product: The product to display.
/// - presentationStyle: How to present **native** detail (ignored for web).
/// - isReadOnly: Whether the native screen should be read-only.
/// - onDelete: Optional callback invoked after a successful product delete.
/// - Returns: A ready-to-present view controller (native or web).
func makeDestination(product: Product,
presentationStyle: Presentation = .push,
isReadOnly: Bool,
onDismissWeb: (() -> Void)? = nil,
onDelete: (() -> Void)? = nil) -> UIViewController {

let viewController: UIViewController
if shouldOpenInWeb(product: product) {
let coordinator = coordinatorFactory.webCoordinator(site: stores.sessionManager.defaultSite)
viewController = coordinator.viewController(product: product) {
onDismissWeb?()
}

} else {
let coordinator = coordinatorFactory.nativeCoordinator()
viewController = coordinator.viewController(product: product,
presentationStyle: presentationStyle,
isReadOnly: isReadOnly,
onDelete: onDelete)
}

return viewController
}

private func shouldOpenInWeb(product: Product) -> Bool {
return ciabChecker.isCurrentSiteCIAB && product.productType == .booking
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,10 @@ private extension ProductLoaderViewController {
/// Presents the ProductFormViewController, as a childViewController, for a given Product.
///
func presentProductDetails(for product: Product) {
ProductDetailsFactory.productDetails(product: product,
presentationStyle: .contained(containerViewController: { [weak self] in self }),
forceReadOnly: forceReadOnly) { [weak self] viewController in
self?.attachProductDetailsChildViewController(viewController)
}
let viewController = ProductDetailNavigator.shared.makeDestination(product: product,
presentationStyle: .contained(in: { [weak self] in self }),
isReadOnly: forceReadOnly)
attachProductDetailsChildViewController(viewController)
}

/// Presents the product variation details for a given ProductVariation and its parent Product.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,19 @@ struct ProductDetailsFactory {
/// - currencySettings: site currency settings.
/// - forceReadOnly: force the product detail to be presented in read only mode
/// - onDeleteCompletion: called when the product deletion completes in the product form.
/// - onCompletion: called when the view controller is created and ready for display.
static func productDetails(product: Product,
presentationStyle: ProductFormPresentationStyle,
currencySettings: CurrencySettings = ServiceLocator.currencySettings,
forceReadOnly: Bool,
productImageUploader: ProductImageUploaderProtocol = ServiceLocator.productImageUploader,
onDeleteCompletion: @escaping () -> Void = {},
onCompletion: @escaping (UIViewController) -> Void) {
onDeleteCompletion: @escaping () -> Void = {}) -> UIViewController {
let vc = productDetails(product: product,
presentationStyle: presentationStyle,
currencySettings: currencySettings,
isEditProductsEnabled: forceReadOnly ? false: true,
productImageUploader: productImageUploader,
onDeleteCompletion: onDeleteCompletion)
onCompletion(vc)
return vc
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,19 @@ private extension ProductsSplitViewCoordinator {
}

func showProductForm(product: Product) {
ProductDetailsFactory.productDetails(product: product,
presentationStyle: .navigationStack,
forceReadOnly: false,
onDeleteCompletion: { [weak self] in
self?.onSecondaryProductFormDeletion()
}) { [weak self] viewController in
self?.showSecondaryView(contentType: .productForm(product: product), viewController: viewController, replacesNavigationStack: true)
}
let viewController = ProductDetailNavigator.shared.makeDestination(
product: product,
isReadOnly: false,
onDismissWeb: { [weak self] in
self?.resyncProducts()
},
onDelete: { [weak self] in
self?.onSecondaryProductFormDeletion()
})

showSecondaryView(contentType: .productForm(product: product),
viewController: viewController,
replacesNavigationStack: true)
}

func startProductCreationIfNoUnsavedChanges(sourceView: AddProductCoordinator.SourceView, isFirstProduct: Bool) {
Expand Down Expand Up @@ -194,6 +199,11 @@ private extension ProductsSplitViewCoordinator {
}
}

func resyncProducts() {
guard let productsViewController = primaryNavigationController.topViewController as? ProductsViewController else { return }
productsViewController.resync()
}

func showEmptyViewOrFirstProduct() {
showEmptyView()
switch primaryNavigationController.topViewController {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,11 @@ final class ProductsViewController: UIViewController, GhostableViewController {
func startProductCreation() {
addProduct(sourceBarButtonItem: addProductButton, isFirstProduct: false)
}

func resync() {
tableView.reloadData()
paginationTracker.resync()
}
}

// MARK: - Navigation Bar Actions
Expand Down Expand Up @@ -1214,11 +1219,9 @@ extension ProductsViewController: UITableViewDelegate {
private extension ProductsViewController {
func didSelectProduct(product: Product) {
guard isSplitViewEnabled else {
ProductDetailsFactory.productDetails(product: product,
presentationStyle: .navigationStack,
forceReadOnly: false) { [weak self] viewController in
self?.navigationController?.pushViewController(viewController, animated: true)
}
let viewController = ProductDetailNavigator.shared.makeDestination(product: product,
isReadOnly: false)
navigationController?.pushViewController(viewController, animated: true)
return
}
navigateToContent(.productForm(product: product))
Expand Down
Loading