diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration.xcodeproj/project.pbxproj b/Samples/MobileBuyIntegration/MobileBuyIntegration.xcodeproj/project.pbxproj index d54e17bb0..bde6cfe79 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration.xcodeproj/project.pbxproj +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 6A257A152AFBB06300610DA5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A257A142AFBB06300610DA5 /* Logger.swift */; }; 6A2E77BE2CE606490067062D /* ProductGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A2E77BD2CE606400067062D /* ProductGridView.swift */; }; 6A2E77C02CE618720067062D /* CartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A2E77BF2CE6186F0067062D /* CartView.swift */; }; + CB88888882EE000000000001 /* CheckoutWithPayButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB88888882EE000000000002 /* CheckoutWithPayButton.swift */; }; 6A3393632CEF742500E89FAA /* CheckoutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A3393622CEF742200E89FAA /* CheckoutController.swift */; }; 6A3393652CEF9E8100E89FAA /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A3393642CEF9E7F00E89FAA /* Theme.swift */; }; 6A3467332B600E64007314A8 /* LogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A3467322B600E64007314A8 /* LogsView.swift */; }; @@ -61,6 +62,7 @@ 6A257A142AFBB06300610DA5 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 6A2E77BD2CE606400067062D /* ProductGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductGridView.swift; sourceTree = ""; }; 6A2E77BF2CE6186F0067062D /* CartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartView.swift; sourceTree = ""; }; + CB88888882EE000000000002 /* CheckoutWithPayButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckoutWithPayButton.swift; sourceTree = ""; }; 6A3393622CEF742200E89FAA /* CheckoutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckoutController.swift; sourceTree = ""; }; 6A3393642CEF9E7F00E89FAA /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; 6A3467322B600E64007314A8 /* LogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsView.swift; sourceTree = ""; }; @@ -192,6 +194,7 @@ isa = PBXGroup; children = ( 6A2E77BF2CE6186F0067062D /* CartView.swift */, + CB88888882EE000000000002 /* CheckoutWithPayButton.swift */, 6A2E77BD2CE606400067062D /* ProductGridView.swift */, 6A3467322B600E64007314A8 /* LogsView.swift */, 4EBBA76E2A5F0CE200193E19 /* ProductView.swift */, @@ -334,6 +337,7 @@ 6A257A152AFBB06300610DA5 /* Logger.swift in Sources */, 4EBBA76D2A5F0CE200193E19 /* SceneDelegate.swift in Sources */, 6A2E77C02CE618720067062D /* CartView.swift in Sources */, + CB88888882EE000000000001 /* CheckoutWithPayButton.swift in Sources */, 6A3393652CEF9E8100E89FAA /* Theme.swift in Sources */, CB3613492E98055700BBE31D /* UIView.swift in Sources */, CB476E0F2E97CAC200878439 /* AddressSelectionViewController.swift in Sources */, diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/CartView.swift b/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/CartView.swift index 41f6e0f58..e2f7184a1 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/CartView.swift +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/CartView.swift @@ -35,6 +35,9 @@ struct CartView: View { @ObservedObject var cartManager: CartManager = .shared @ObservedObject var config: AppConfiguration = appConfiguration + @AppStorage(AppStorageKeys.showNativePayButton.rawValue) + var showNativePayButton: Bool = false + var body: some View { if let lines = cartManager.cart?.lines.nodes { ZStack(alignment: .bottom) { @@ -122,102 +125,110 @@ struct CartView: View { } .sheet(isPresented: $showCheckoutSheet) { if let url = cartManager.cart?.checkoutUrl { - ShopifyCheckout(checkout: url) - // .auth(token: "your-auth-token-here") // Uncomment to add authentication - .colorScheme(.automatic) - .onStart { event in - print("Checkout started with cart ID: \(event.cart.id)") - } - .onCancel { - showCheckoutSheet = false - } - .onComplete { event in - showCheckoutSheet = false - // Handle checkout completion - print("Checkout completed with order ID: \(event.orderConfirmation.order.id)") - } - .onFail { error in - showCheckoutSheet = false - // Handle checkout failure - print("Checkout failed: \(error)") - } - .onAddressChangeStart { event in - print( - "🎉 SwiftUI: Address change intent received for addressType: \(event.addressType)" - ) - - // Respond with updated cart after 2 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - let hardcodedAddress = CartDeliveryAddress( - firstName: "Jane", - lastName: "Smith", - address1: "456 SwiftUI Avenue", - address2: "Suite 200", - city: "Vancouver", - countryCode: "CA", - phone: "+1-604-555-0456", - provinceCode: "BC", - zip: "V6B 1A1" - ) - - let selectableAddress = CartSelectableAddress( - address: .deliveryAddress(hardcodedAddress), - selected: true + if showNativePayButton { + CheckoutWithPayButtonView( + checkoutURL: url, + isPresented: $showCheckoutSheet, + showPayButton: true + ) + } else { + ShopifyCheckout(checkout: url) + // .auth(token: "your-auth-token-here") // Uncomment to add authentication + .colorScheme(.automatic) + .onStart { event in + print("Checkout started with cart ID: \(event.cart.id)") + } + .onCancel { + showCheckoutSheet = false + } + .onComplete { event in + showCheckoutSheet = false + // Handle checkout completion + print("Checkout completed with order ID: \(event.orderConfirmation.order.id)") + } + .onFail { error in + showCheckoutSheet = false + // Handle checkout failure + print("Checkout failed: \(error)") + } + .onAddressChangeStart { event in + print( + "Address change intent received for addressType: \(event.addressType)" ) - let delivery = CartDelivery(addresses: [selectableAddress]) - let updatedCart = event.cart.copy( - delivery: .override(delivery) - ) + // Respond with updated cart after 2 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + let hardcodedAddress = CartDeliveryAddress( + firstName: "Jane", + lastName: "Smith", + address1: "456 SwiftUI Avenue", + address2: "Suite 200", + city: "Vancouver", + countryCode: "CA", + phone: "+1-604-555-0456", + provinceCode: "BC", + zip: "V6B 1A1" + ) - let response = CheckoutAddressChangeStartResponsePayload(cart: updatedCart) + let selectableAddress = CartSelectableAddress( + address: .deliveryAddress(hardcodedAddress), + selected: true + ) + let delivery = CartDelivery(addresses: [selectableAddress]) - print("🎉 SwiftUI: Responding with hardcoded Vancouver address") - do { - try event.respondWith(payload: response) - } catch { - print( - "Failed to respondwith: Responding with hardcoded Vancouver address" + let updatedCart = event.cart.copy( + delivery: .override(delivery) ) + + let response = CheckoutAddressChangeStartResponsePayload(cart: updatedCart) + + print("Responding with hardcoded Vancouver address") + do { + try event.respondWith(payload: response) + } catch { + print( + "Failed to respondwith: Responding with hardcoded Vancouver address" + ) + } } } - } - .onPaymentMethodChangeStart { event in - print("🎉 SwiftUI: Payment method change start received") + .onPaymentMethodChangeStart { event in + print("Payment method change start received") - // Respond with updated cart after 2 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - let instrument = CreditCardPaymentInstrument( - externalReferenceId: "card-visa-1234" - ) + // Respond with updated cart after 2 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + let instrument = CreditCardPaymentInstrument( + externalReferenceId: "card-visa-1234" + ) - let paymentMethod = CartPaymentMethod(instruments: [instrument]) - let payment = CartPayment(methods: [paymentMethod]) - - let updatedCart = Cart( - id: event.cart.id, - lines: event.cart.lines, - cost: event.cart.cost, - buyerIdentity: event.cart.buyerIdentity, - deliveryGroups: event.cart.deliveryGroups, - discountCodes: event.cart.discountCodes, - appliedGiftCards: event.cart.appliedGiftCards, - discountAllocations: event.cart.discountAllocations, - delivery: event.cart.delivery, - payment: payment - ) + let paymentMethod = CartPaymentMethod(instruments: [instrument]) + let payment = CartPayment(methods: [paymentMethod]) + + let updatedCart = Cart( + id: event.cart.id, + lines: event.cart.lines, + cost: event.cart.cost, + buyerIdentity: event.cart.buyerIdentity, + deliveryGroups: event.cart.deliveryGroups, + discountCodes: event.cart.discountCodes, + appliedGiftCards: event.cart.appliedGiftCards, + discountAllocations: event.cart.discountAllocations, + delivery: event.cart.delivery, + payment: payment + ) - let response = CheckoutPaymentMethodChangeStartResponsePayload(cart: updatedCart) + let response = CheckoutPaymentMethodChangeStartResponsePayload(cart: updatedCart) - print("🎉 SwiftUI: Responding with hardcoded Visa ending in 1234") - do { - try event.respondWith(payload: response) - } catch { - print("Failed to respond with payment method change") + print("Responding with hardcoded Visa ending in 1234") + do { + try event.respondWith(payload: response) + } catch { + print("Failed to respond with payment method change") + } } } - } - .edgesIgnoringSafeArea(.all) + .edgesIgnoringSafeArea(.all) + } } } } else { diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/CheckoutWithPayButton.swift b/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/CheckoutWithPayButton.swift new file mode 100644 index 000000000..cf6c497c6 --- /dev/null +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/CheckoutWithPayButton.swift @@ -0,0 +1,175 @@ +/* + MIT License + + Copyright 2023 - Present, Shopify Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import ShopifyCheckoutSheetKit +import SwiftUI +import UIKit + +struct CheckoutWithPayButtonView: View { + let checkoutURL: URL + @Binding var isPresented: Bool + let showPayButton: Bool + + @State private var submitAction: (() async throws -> CheckoutSubmitResponsePayload)? + @State private var isSubmitting = false + + var body: some View { + ZStack(alignment: .bottom) { + CheckoutViewRepresentable( + checkoutURL: checkoutURL, + isPresented: $isPresented, + onSubmitReady: { action in + submitAction = action + } + ) + + if showPayButton { + NativePayButtonOverlay(isSubmitting: $isSubmitting) { + guard let submit = submitAction else { return } + isSubmitting = true + Task { + do { + let response = try await submit() + print("Checkout submit succeeded: \(response)") + } catch { + print("Checkout submit failed: \(error)") + } + isSubmitting = false + } + } + } + } + .edgesIgnoringSafeArea(.all) + } +} + +struct NativePayButtonOverlay: View { + @Binding var isSubmitting: Bool + let onTap: () -> Void + + var body: some View { + VStack { + Button(action: onTap) { + HStack { + if isSubmitting { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(0.8) + } + Text("Pay now") + .fontWeight(.bold) + .foregroundColor(.white) + } + .frame(maxWidth: .infinity) + .padding() + .background(Color(ColorPalette.primaryColor)) + .cornerRadius(DesignSystem.cornerRadius) + } + .disabled(isSubmitting) + } + .padding() + .background(Color.white) + } +} + +struct CheckoutViewRepresentable: UIViewControllerRepresentable { + let checkoutURL: URL + @Binding var isPresented: Bool + var onSubmitReady: ((@escaping () async throws -> CheckoutSubmitResponsePayload) -> Void)? + + func makeCoordinator() -> Coordinator { + Coordinator(parent: self) + } + + func makeUIViewController(context: Context) -> CheckoutViewController { + let viewController = CheckoutViewController( + checkout: checkoutURL, + delegate: context.coordinator + ) + context.coordinator.checkoutViewController = viewController + return viewController + } + + func updateUIViewController(_ uiViewController: CheckoutViewController, context: Context) { + guard + let webViewController = uiViewController + .viewControllers + .compactMap({ $0 as? CheckoutWebViewController }) + .first + else { + return + } + + context.coordinator.webViewController = webViewController + + onSubmitReady? { + try await webViewController.submit() + } + } + + class Coordinator: NSObject, CheckoutDelegate { + var parent: CheckoutViewRepresentable + weak var checkoutViewController: CheckoutViewController? + weak var webViewController: CheckoutWebViewController? + + init(parent: CheckoutViewRepresentable) { + self.parent = parent + } + + func checkoutDidStart(event: CheckoutStartEvent) { + print("Checkout started with cart ID: \(event.cart.id)") + } + + func checkoutDidComplete(event: CheckoutCompleteEvent) { + parent.isPresented = false + print("Checkout completed with order ID: \(event.orderConfirmation.order.id)") + } + + func checkoutDidCancel() { + parent.isPresented = false + } + + func checkoutDidFail(error: CheckoutError) { + parent.isPresented = false + print("Checkout failed: \(error)") + } + + func checkoutDidClickLink(url: URL) { + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + } + + func checkoutDidStartAddressChange(event: CheckoutAddressChangeStartEvent) { + print("Address change started for type: \(event.addressType)") + } + + func checkoutDidStartSubmit(event: CheckoutSubmitStartEvent) { + print("Submit started") + } + + func checkoutDidStartPaymentMethodChange(event: CheckoutPaymentMethodChangeStartEvent) { + print("Payment method change started") + } + } +} diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/SettingsView.swift b/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/SettingsView.swift index 308cea12e..124c1cdf4 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/SettingsView.swift +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/SettingsView.swift @@ -29,6 +29,7 @@ import SwiftUI enum AppStorageKeys: String { case acceleratedCheckoutsLogLevel case checkoutSheetKitLogLevel + case showNativePayButton } struct SettingsView: View { @@ -50,6 +51,9 @@ struct SettingsView: View { } } + @AppStorage(AppStorageKeys.showNativePayButton.rawValue) + var showNativePayButton: Bool = false + @State private var preloadingEnabled = ShopifyCheckoutSheetKit.configuration.preloading.enabled @State private var logs: [String?] = LogReader.shared.readLogs() ?? [] @State private var selectedColorScheme = ShopifyCheckoutSheetKit.configuration.colorScheme @@ -65,6 +69,7 @@ struct SettingsView: View { ShopifyCheckoutSheetKit.configuration.preloading.enabled = newValue } Toggle("Prefill buyer information", isOn: $config.useVaultedState) + Toggle("Show native pay button", isOn: $showNativePayButton) } Section(header: Text("Debug")) {