Skip to content

Commit 1fac92b

Browse files
authored
Open CIAB booking products in web (#16250)
2 parents f3d19d1 + 5002b37 commit 1fac92b

20 files changed

+349
-79
lines changed

WooCommerce/Classes/Authentication/AuthenticatedWebViewController.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ final class AuthenticatedWebViewController: UIViewController {
123123
viewModel.handleDismissal()
124124
}
125125
}
126+
127+
override func viewDidDisappear(_ animated: Bool) {
128+
viewModel.handleDisappear()
129+
super.viewDidDisappear(animated)
130+
}
126131
}
127132

128133
private extension AuthenticatedWebViewController {

WooCommerce/Classes/Authentication/AuthenticatedWebViewModel.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ protocol AuthenticatedWebViewModel {
2222
/// Triggered when the web view is dismissed
2323
func handleDismissal()
2424

25+
/// Triggered when the web view is disappeared
26+
func handleDisappear()
27+
2528
/// Triggered when the web view redirects to a new URL
2629
func handleRedirect(for url: URL?)
2730

@@ -53,6 +56,10 @@ extension AuthenticatedWebViewModel {
5356
func didFailProvisionalNavigation(with error: Error) {
5457
// NO-OP
5558
}
59+
60+
func handleDisappear () {
61+
// NO-OP
62+
}
5663
}
5764

5865
// MARK: - Helper methods

WooCommerce/Classes/Authentication/WPAdminWebViewModel.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ class WPAdminWebViewModel: AuthenticatedWebViewModel, WebviewReloadable {
2020
// no-op
2121
}
2222

23+
func handleDisappear() {
24+
// no-op
25+
}
26+
2327
func handleRedirect(for url: URL?) {
2428
guard let url else {
2529
return

WooCommerce/Classes/CIAB/CIABEligibilityChecker.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
/// periphery: ignore:all - Will be used in upcoming PRs
2-
31
import Foundation
42
import Yosemite
53

WooCommerce/Classes/CIAB/CIABEligibilityCheckerProtocol.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import Foundation
22
import Yosemite
33

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Yosemite
2+
import UIKit
3+
4+
/// Coordinator for the **native** product detail/editor flow.
5+
/// Delegates VC construction to `ProductDetailsFactory` and applies the requested presentation style.
6+
class ProductDetailNativeCoordinator {
7+
8+
func viewController(
9+
product: Product,
10+
presentationStyle: ProductDetailNavigator.Presentation,
11+
isReadOnly: Bool,
12+
onDelete: (() -> Void)? = nil) -> UIViewController {
13+
return ProductDetailsFactory.productDetails(product: product,
14+
presentationStyle: presentationStyle.asProductFormPresentationStyle,
15+
forceReadOnly: isReadOnly,
16+
onDeleteCompletion: onDelete ?? {})
17+
}
18+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import UIKit
2+
import Yosemite
3+
4+
/// Coordinator for the **admin web** product detail/editor flow.
5+
class ProductDetailWebCoordinator: NSObject {
6+
private let site: Site?
7+
8+
init(site: Site?) {
9+
self.site = site
10+
}
11+
12+
func viewController(product: Product, onDismiss: @escaping () -> Void) -> UIViewController {
13+
guard let url = ProductAdminURLProvider.editURL(for: product, site: site) else {
14+
return UIViewController()
15+
}
16+
17+
let viewModel = AdminWebViewModel(title: product.name, initialURL: url) { [onDismiss] in
18+
onDismiss()
19+
}
20+
let webViewController = AuthenticatedWebViewController(viewModel: viewModel)
21+
return webViewController
22+
}
23+
}
24+
25+
fileprivate class AdminWebViewModel: WPAdminWebViewModel {
26+
let onDismiss: (() -> Void)
27+
28+
init(title: String, initialURL: URL, onDismiss: @escaping () -> Void) {
29+
self.onDismiss = onDismiss
30+
super.init(title: title, initialURL: initialURL)
31+
}
32+
33+
override func handleDisappear() {
34+
onDismiss()
35+
super.handleDisappear()
36+
}
37+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Yosemite
2+
import Foundation
3+
4+
/// Builds canonical admin edit URLs for products.
5+
enum ProductAdminURLProvider {
6+
7+
static func editURL(for product: Product, site: Site?) -> URL? {
8+
guard let base = site?.adminURLWithFallback() else { return nil }
9+
10+
var components = URLComponents(url: base, resolvingAgainstBaseURL: false)!
11+
components.queryItems = [
12+
URLQueryItem(name: "page", value: "next-admin"),
13+
URLQueryItem(name: "p", value: "/woocommerce/products/edit/\(product.productID)")
14+
]
15+
16+
return components.url
17+
}
18+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Yosemite
2+
3+
/// Factory for producing coordinators used by the navigator.
4+
protocol ProductDetailCoordinatorFactoryProtocol {
5+
func webCoordinator(site: Site?) -> ProductDetailWebCoordinator
6+
func nativeCoordinator() -> ProductDetailNativeCoordinator
7+
}
8+
9+
/// Default coordinator factory that wires production dependencies.
10+
class ProductDetailCoordinatorFactory: ProductDetailCoordinatorFactoryProtocol {
11+
static let `default` = ProductDetailCoordinatorFactory()
12+
13+
func webCoordinator(site: Site?) -> ProductDetailWebCoordinator {
14+
return ProductDetailWebCoordinator(site: site)
15+
}
16+
17+
func nativeCoordinator() -> ProductDetailNativeCoordinator {
18+
return ProductDetailNativeCoordinator()
19+
}
20+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import UIKit
2+
import Yosemite
3+
4+
/// Decides between **native** and **admin web** product detail flows and builds the destination VC.
5+
final class ProductDetailNavigator {
6+
/// Describes how the **native** destination should be presented.
7+
enum Presentation {
8+
case push
9+
case contained(in: () -> UIViewController?)
10+
11+
var asProductFormPresentationStyle: ProductFormPresentationStyle {
12+
switch self {
13+
case .contained(let inVC):
14+
.contained(containerViewController: inVC)
15+
case .push:
16+
.navigationStack
17+
}
18+
}
19+
}
20+
21+
static var shared = ProductDetailNavigator()
22+
23+
private let ciabChecker: CIABEligibilityCheckerProtocol
24+
private let coordinatorFactory: ProductDetailCoordinatorFactoryProtocol
25+
private let stores: StoresManager
26+
27+
init(ciabChecker: CIABEligibilityCheckerProtocol = CIABEligibilityChecker(),
28+
coordinatorFactory: ProductDetailCoordinatorFactoryProtocol = ProductDetailCoordinatorFactory.default,
29+
stores: StoresManager = ServiceLocator.stores,
30+
) {
31+
self.ciabChecker = ciabChecker
32+
self.coordinatorFactory = coordinatorFactory
33+
self.stores = stores
34+
}
35+
36+
/// Builds the destination `UIViewController` for the given product.
37+
/// - Parameters:
38+
/// - product: The product to display.
39+
/// - presentationStyle: How to present **native** detail (ignored for web).
40+
/// - isReadOnly: Whether the native screen should be read-only.
41+
/// - onDelete: Optional callback invoked after a successful product delete.
42+
/// - Returns: A ready-to-present view controller (native or web).
43+
func makeDestination(product: Product,
44+
presentationStyle: Presentation = .push,
45+
isReadOnly: Bool,
46+
onDismissWeb: (() -> Void)? = nil,
47+
onDelete: (() -> Void)? = nil) -> UIViewController {
48+
49+
let viewController: UIViewController
50+
if shouldOpenInWeb(product: product) {
51+
let coordinator = coordinatorFactory.webCoordinator(site: stores.sessionManager.defaultSite)
52+
viewController = coordinator.viewController(product: product) {
53+
onDismissWeb?()
54+
}
55+
56+
} else {
57+
let coordinator = coordinatorFactory.nativeCoordinator()
58+
viewController = coordinator.viewController(product: product,
59+
presentationStyle: presentationStyle,
60+
isReadOnly: isReadOnly,
61+
onDelete: onDelete)
62+
}
63+
64+
return viewController
65+
}
66+
67+
private func shouldOpenInWeb(product: Product) -> Bool {
68+
return ciabChecker.isCurrentSiteCIAB && product.productType == .booking
69+
}
70+
}

0 commit comments

Comments
 (0)