Skip to content
Open
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
@@ -0,0 +1,117 @@
import Combine
import ShopifyCheckoutKit
import SwiftUI
import UIKit

@MainActor
final class CartCheckoutPresentation: ObservableObject, CheckoutDelegate {
@Published var showCheckoutSheet = false

@AppStorage(AppStorageKeys.windowOpenHandler.rawValue)
private var windowOpenHandler: WindowOpenHandlerOption = .default

private var isCompleted = false
private var selectedAddressIDs: [String: String] = [:]
private var selectedDeliveryMethodIDs: [String: String] = [:]
private let resetCart: @MainActor () -> Void

init(resetCart: @escaping @MainActor () -> Void = { CartManager.shared.resetCart() }) {
self.resetCart = resetCart
}

func present(
checkout url: URL,
using option: CheckoutPresentationOption,
from presenter: UIViewController?
) {
switch option {
case .swiftUI:
showCheckoutSheet = true
case .uiKit:
guard let presenter else { return }
ShopifyCheckoutKit.configuration.appearance = .app(.automatic)
let checkoutViewController = CheckoutViewController(checkout: url, delegate: self)
presenter.present(checkoutViewController, animated: true)
}
}

/// Records a completed checkout. The cart is reset on dismissal so the confirmation page stays visible:
/// resetting now would nil the cart and SwiftUI would auto-collapse the sheet.
func recordCompletion() {
isCompleted = true
}

func resetCompletedCart() {
if isCompleted {
isCompleted = false
resetCart()
}
selectedAddressIDs = [:]
selectedDeliveryMethodIDs = [:]
}

func checkoutDidStart(_ event: CheckoutStartEvent) {
print("[CheckoutKitSwiftDemo] Started: \(event.checkout.id)")
}

func checkoutDidUpdate(_ event: CheckoutUpdateEvent) {
let updatedAddressIDs = Self.selectedAddressIDs(in: event.checkout)
if updatedAddressIDs != selectedAddressIDs {
print("[CheckoutKitSwiftDemo] Selected address changed")
selectedAddressIDs = updatedAddressIDs
}

let updatedDeliveryMethodIDs = Self.selectedDeliveryMethodIDs(in: event.checkout)
if updatedDeliveryMethodIDs != selectedDeliveryMethodIDs {
print("[CheckoutKitSwiftDemo] Selected delivery method changed")
selectedDeliveryMethodIDs = updatedDeliveryMethodIDs
}

print("[CheckoutKitSwiftDemo] Updated: \(event.checkout.id)")
}

func checkoutDidComplete(_ event: CheckoutCompleteEvent) {
print("[CheckoutKitSwiftDemo] Completed: \(event.checkout.order?.id ?? "unknown")")
recordCompletion()
}

func checkoutAction(for link: CheckoutLink) -> CheckoutLinkAction {
print("[CheckoutKitSwiftDemo] Checkout link clicked: \(link.url)")
switch windowOpenHandler {
case .default:
print("[CheckoutKitSwiftDemo] Checkout link delegated to Checkout Kit: \(link.url)")
return .open
case .externalApp:
print("[CheckoutKitSwiftDemo] Checkout link handled by sample: \(link.url)")
UIApplication.shared.open(link.url)
return .handled
}
}

func checkoutDidDismiss() {
print("[CheckoutKitSwiftDemo] Dismissed")
showCheckoutSheet = false
resetCompletedCart()
}

func checkoutDidFail(_ event: CheckoutFailureEvent) {
showCheckoutSheet = false
print("[CheckoutKitSwiftDemo] Failed: \(event.error)")
}

private static func selectedAddressIDs(in checkout: ShopifyCheckoutKit.Checkout) -> [String: String] {
(checkout.fulfillment?.methods ?? []).reduce(into: [:]) { selections, method in
guard method.type == "shipping", let destinationID = method.selectedDestinationID else { return }
selections[method.id] = destinationID
}
}

private static func selectedDeliveryMethodIDs(in checkout: ShopifyCheckoutKit.Checkout) -> [String: String] {
(checkout.fulfillment?.methods ?? []).reduce(into: [:]) { selections, method in
for group in method.groups ?? [] {
guard let optionID = group.selectedOptionID else { continue }
selections["\(method.id):\(group.id)"] = optionID
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,18 @@ typealias CartLineNode = Storefront.CartFragment.Lines.Node
struct CartView: View {
@State var cartCompleted: Bool = false
@State var isBusy: Bool = false
@State var isCompleted: Bool = false
@State var showCheckoutSheet: Bool = false
@StateObject private var checkoutPresentation = CartCheckoutPresentation()
@State private var checkoutPreload: CheckoutPreload?
@State private var preloadStateTestId = PreloadStateMarker.testId(for: .idle)
@State private var selectedAddressIDs: [String: String] = [:]
@State private var selectedDeliveryMethodIDs: [String: String] = [:]

@ObservedObject var cartManager: CartManager = .shared
@ObservedObject private var preloadCacheHitLog: PreloadCacheHitLog = .shared

@AppStorage(AppStorageKeys.applePayStyle.rawValue)
var applePayStyle: ApplePayStyleOption = .automatic

@AppStorage(AppStorageKeys.windowOpenHandler.rawValue)
var windowOpenHandler: WindowOpenHandlerOption = .default
@AppStorage(AppStorageKeys.checkoutPresentation.rawValue)
var checkoutPresentationOption: CheckoutPresentationOption = .swiftUI

@AppStorage(AppStorageKeys.checkoutPreloadingEnabled.rawValue)
var checkoutPreloadingEnabled = true
Expand Down Expand Up @@ -62,24 +59,24 @@ struct CartView: View {
.onStart { event in
print("[AcceleratedCheckout] Started: \(event.checkout.id)")
}
.onUpdate(handleCheckoutUpdate)
.onUpdate(checkoutPresentation.checkoutDidUpdate)
.onComplete { event in
print("[AcceleratedCheckout] Completed: \(event.checkout.order?.id ?? "unknown")")
isCompleted = true
checkoutPresentation.recordCompletion()
}
.onLinkClick(handleCheckoutLink)
.onLinkClick(checkoutPresentation.checkoutAction(for:))
.onFail { error in
print("[AcceleratedCheckout] Failed: \(error)")
}
.onDismiss {
print("[AcceleratedCheckout] Dismissed")
resetCompletedCart()
checkoutPresentation.resetCompletedCart()
}
}
}

Button(
action: { showCheckoutSheet = true },
action: presentCheckout,
label: {
HStack {
Text("Check out")
Expand All @@ -106,32 +103,16 @@ struct CartView: View {
.padding(.horizontal, 20)
.padding(.bottom, 20)
}
.sheet(isPresented: $showCheckoutSheet) {
.sheet(isPresented: $checkoutPresentation.showCheckoutSheet) {
if let url = cartManager.cart?.checkoutURL {
ShopifyCheckout(checkout: url)
.appearance(.app(.automatic))
.onStart { event in
print("[CheckoutKitSwiftDemo] Started: \(event.checkout.id)")
}
.onUpdate(handleCheckoutUpdate)
.onLinkClick(handleCheckoutLink)
.onComplete { event in
// Set the flag here; defer the cart reset until the user dismisses
// the sheet (in .onDismiss). Resetting now would nil the cart and
// SwiftUI would auto-collapse this sheet, hiding the confirmation page.
print("[CheckoutKitSwiftDemo] Completed: \(event.checkout.order?.id ?? "unknown")")
isCompleted = true
}
.onDismiss {
print("[CheckoutKitSwiftDemo] Dismissed")
showCheckoutSheet = false

resetCompletedCart()
}
.onFail { event in
showCheckoutSheet = false
print("[CheckoutKitSwiftDemo] Failed: \(event.error)")
}
.onStart(checkoutPresentation.checkoutDidStart)
.onUpdate(checkoutPresentation.checkoutDidUpdate)
.onLinkClick(checkoutPresentation.checkoutAction(for:))
.onComplete(checkoutPresentation.checkoutDidComplete)
.onDismiss(checkoutPresentation.checkoutDidDismiss)
.onFail(checkoutPresentation.checkoutDidFail)
.edgesIgnoringSafeArea(.all)
}
}
Expand Down Expand Up @@ -161,9 +142,13 @@ struct CartView: View {
}

private func presentCheckout() {
guard let url = CartManager.shared.cart?.checkoutURL else { return }
guard let url = cartManager.cart?.checkoutURL else { return }

CheckoutCoordinator.shared?.present(checkout: url)
checkoutPresentation.present(
checkout: url,
using: checkoutPresentationOption,
from: CheckoutCoordinator.shared?.window?.topMostViewController()
)
}

private func preloadCheckoutIfNeeded() {
Expand All @@ -177,60 +162,6 @@ struct CartView: View {
ShopifyCheckoutKit.configuration.logger.log("Preload state changed to \(state)")
}
}

private func resetCompletedCart() {
if isCompleted {
CartManager.shared.resetCart()
isCompleted = false
}
selectedAddressIDs = [:]
selectedDeliveryMethodIDs = [:]
}

private func handleCheckoutUpdate(_ event: CheckoutUpdateEvent) {
let updatedAddressIDs = selectedAddressIDs(in: event.checkout)
if updatedAddressIDs != selectedAddressIDs {
print("[CheckoutKitSwiftDemo] Selected address changed")
selectedAddressIDs = updatedAddressIDs
}

let updatedDeliveryMethodIDs = selectedDeliveryMethodIDs(in: event.checkout)
if updatedDeliveryMethodIDs != selectedDeliveryMethodIDs {
print("[CheckoutKitSwiftDemo] Selected delivery method changed")
selectedDeliveryMethodIDs = updatedDeliveryMethodIDs
}

print("[CheckoutKitSwiftDemo] Updated: \(event.checkout.id)")
}

private func handleCheckoutLink(_ link: CheckoutLink) -> CheckoutLinkAction {
print("[CheckoutKitSwiftDemo] Checkout link clicked: \(link.url)")
switch windowOpenHandler {
case .default:
print("[CheckoutKitSwiftDemo] Checkout link delegated to Checkout Kit: \(link.url)")
return .open
case .externalApp:
print("[CheckoutKitSwiftDemo] Checkout link handled by sample: \(link.url)")
UIApplication.shared.open(link.url)
return .handled
}
}

private func selectedAddressIDs(in checkout: ShopifyCheckoutKit.Checkout) -> [String: String] {
(checkout.fulfillment?.methods ?? []).reduce(into: [:]) { selections, method in
guard method.type == "shipping", let destinationID = method.selectedDestinationID else { return }
selections[method.id] = destinationID
}
}

private func selectedDeliveryMethodIDs(in checkout: ShopifyCheckoutKit.Checkout) -> [String: String] {
(checkout.fulfillment?.methods ?? []).reduce(into: [:]) { selections, method in
for group in method.groups ?? [] {
guard let optionID = group.selectedOptionID else { continue }
selections["\(method.id):\(group.id)"] = optionID
}
}
}
}

struct EmptyState: View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import WebKit
enum AppStorageKeys: String {
case acceleratedCheckoutsLogLevel
case checkoutKitLogLevel
case checkoutPresentation
case checkoutPreloadingEnabled
case preloadObservabilityEnabled
case buyerIdentityMode
Expand All @@ -31,9 +32,24 @@ enum WindowOpenHandlerOption: String, CaseIterable {
}
}

enum CheckoutPresentationOption: String, CaseIterable {
case swiftUI
case uiKit

var title: String {
switch self {
case .swiftUI: return "SwiftUI"
case .uiKit: return "UIKit"
}
}
}

struct SettingsView: View {
@ObservedObject var config: AppConfiguration = appConfiguration

@AppStorage(AppStorageKeys.checkoutPresentation.rawValue)
var checkoutPresentationOption: CheckoutPresentationOption = .swiftUI

@AppStorage(AppStorageKeys.checkoutKitLogLevel.rawValue)
var checkoutKitLogLevel: LogLevel = .debug {
didSet {
Expand All @@ -57,7 +73,22 @@ struct SettingsView: View {
var body: some View {
NavigationView {
List {
Section(header: Text("Features")) {
Section(
header: Text("Features"),
footer: Text("Checkout presentation applies to the Cart tab.")
) {
HStack {
Text("Checkout presentation")
Spacer()
Picker("Checkout presentation", selection: $checkoutPresentationOption) {
ForEach(CheckoutPresentationOption.allCases, id: \.self) { option in
Text(option.title).tag(option)
}
}
.labelsHidden()
.pickerStyle(.menu)
}

Toggle("Checkout preloading", isOn: $checkoutPreloadingEnabled)
.onChange(of: checkoutPreloadingEnabled) { _ in
ShopifyCheckoutKit.configure {
Expand Down
Loading
Loading