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 @@ -67,18 +67,36 @@ public func invalidate() {
CheckoutWebView.preloadCache.evict(with: .idle)
}

/// Presents checkout, or returns the existing checkout in the presenter’s modal hierarchy.
/// Duplicate calls preserve the original checkout URL and delegate, including during dismissal.
@MainActor
@discardableResult
public func present(checkout url: URL, from: UIViewController, delegate: (any CheckoutDelegate)? = nil) -> CheckoutViewController {
let decorated = CheckoutURLDecorator.decorate(url)
let viewController = CheckoutViewController(checkout: decorated, delegate: delegate)
from.present(viewController, animated: true)
return viewController
presentCheckout(checkout: url, from: from, delegate: delegate)
}

@MainActor
@discardableResult
package func present(checkout url: URL, from: UIViewController, entryPoint: MetaData.EntryPoint, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil) -> CheckoutViewController {
presentCheckout(checkout: url, from: from, delegate: delegate, client: client, entryPoint: entryPoint)
}

@MainActor
private func presentCheckout(checkout url: URL, from: UIViewController, delegate: (any CheckoutDelegate)?, client: (any CheckoutCommunicationProtocol)? = nil, entryPoint: MetaData.EntryPoint? = nil) -> CheckoutViewController {
// Start at the root so calls from a checkout child or a modal above it share the same guard.
var root = from
while let ancestor = root.parent ?? root.presentingViewController {
root = ancestor
}
var presented: UIViewController? = root
while let current = presented {
if let checkout = current as? CheckoutViewController {
OSLogger.shared.warn("Checkout is already presented; ignoring duplicate presentation.")
return checkout
}
presented = current.presentedViewController
}

let decorated = CheckoutURLDecorator.decorate(url)
let viewController = CheckoutViewController(checkout: decorated, delegate: delegate, client: client, entryPoint: entryPoint)
from.present(viewController, animated: true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,70 @@ class ShopifyCheckoutKitTests: XCTestCase {
XCTAssertNotNil(webViewController.checkoutView?.client)
}

func test_present_duplicatePreservesCheckoutAndDelegate() throws {
let presenter = RecordingCheckoutPresenter()
let originalDelegate = MockCheckoutDelegate()
let checkout = ShopifyCheckoutKit.present(checkout: checkoutURL, from: presenter, delegate: originalDelegate)

let duplicate = try ShopifyCheckoutKit.present(
checkout: XCTUnwrap(URL(string: "https://shop.example/checkouts/cn/other")),
from: presenter,
delegate: MockCheckoutDelegate()
)

XCTAssertTrue(duplicate === checkout)
XCTAssertEqual(presenter.presentationCount, 1)
let content = try XCTUnwrap(checkout.viewControllers.first as? CheckoutWebViewController)
XCTAssertTrue(content.delegate === originalDelegate)
}

func test_present_fromCheckoutOrItsChildReturnsExistingCheckout() throws {
let presenter = RecordingCheckoutPresenter()
let checkout = ShopifyCheckoutKit.present(checkout: checkoutURL, from: presenter)
let child = try XCTUnwrap(checkout.viewControllers.first)

XCTAssertTrue(ShopifyCheckoutKit.present(checkout: checkoutURL, from: checkout) === checkout)
XCTAssertTrue(ShopifyCheckoutKit.present(checkout: checkoutURL, from: child) === checkout)
XCTAssertEqual(presenter.presentationCount, 1)
}

func test_present_fromContainerChildFindsPresentedCheckout() {
let presenter = RecordingCheckoutPresenter()
let child = UIViewController()
presenter.addChild(child)
let checkout = ShopifyCheckoutKit.present(checkout: checkoutURL, from: presenter)

XCTAssertTrue(ShopifyCheckoutKit.present(checkout: checkoutURL, from: child) === checkout)
XCTAssertEqual(presenter.presentationCount, 1)
}

func test_present_acceleratedEntryPointUsesSameGuard() {
let presenter = RecordingCheckoutPresenter()
let checkout = ShopifyCheckoutKit.present(checkout: checkoutURL, from: presenter, entryPoint: .acceleratedCheckouts)

XCTAssertTrue(ShopifyCheckoutKit.present(checkout: checkoutURL, from: presenter) === checkout)
XCTAssertTrue(ShopifyCheckoutKit.present(checkout: checkoutURL, from: presenter, entryPoint: .acceleratedCheckouts) === checkout)
XCTAssertEqual(presenter.presentationCount, 1)
}

func test_present_afterDismissalCreatesNewCheckoutEvenWhenOldControllerIsRetained() {
let presenter = RecordingCheckoutPresenter()
let checkout = ShopifyCheckoutKit.present(checkout: checkoutURL, from: presenter)
presenter.modal = nil

let next = ShopifyCheckoutKit.present(checkout: checkoutURL, from: presenter)

XCTAssertFalse(next === checkout)
XCTAssertEqual(presenter.presentationCount, 2)
}

func test_present_independentPresentersCanPresentTheirOwnCheckout() {
let first = ShopifyCheckoutKit.present(checkout: checkoutURL, from: RecordingCheckoutPresenter())
let second = ShopifyCheckoutKit.present(checkout: checkoutURL, from: RecordingCheckoutPresenter())

XCTAssertFalse(first === second)
}

func test_logger_withDifferentLogLevels_shouldHaveCorrectLogLevel() {
ShopifyCheckoutKit.configuration.logLevel = .debug
XCTAssertEqual(
Expand Down Expand Up @@ -142,3 +206,19 @@ class ShopifyCheckoutKitTests: XCTestCase {
)
}
}

@MainActor
private final class RecordingCheckoutPresenter: UIViewController {
var modal: UIViewController?
private(set) var presentationCount = 0

override var presentedViewController: UIViewController? {
modal
}

override func present(_ viewControllerToPresent: UIViewController, animated _: Bool, completion: (() -> Void)? = nil) {
presentationCount += 1
modal = viewControllerToPresent
completion?()
}
}
Loading