From 1eb3a52ebbfc41a4550e488e9f1c128b1c2fc31a Mon Sep 17 00:00:00 2001 From: Mark Murray Date: Wed, 6 Aug 2025 15:30:23 +0100 Subject: [PATCH] Render loading state for Accelerated Checkout buttons --- .../MobileBuyIntegration/Views/CartView.swift | 51 +++++++++---- .../Views/ProductView.swift | 40 +++++++--- .../Views/SkeletonButton.swift | 74 +++++++++++++++++++ 3 files changed, 143 insertions(+), 22 deletions(-) create mode 100644 Samples/MobileBuyIntegration/MobileBuyIntegration/Views/SkeletonButton.swift diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/CartView.swift b/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/CartView.swift index c41d881f6..8d59c78d0 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/CartView.swift +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/CartView.swift @@ -33,6 +33,7 @@ struct CartView: View { @State var cartCompleted: Bool = false @State var isBusy: Bool = false @State var showCheckoutSheet: Bool = false + @State var acceleratedCheckoutState: RenderState = .loading @ObservedObject var cartManager: CartManager = .shared @ObservedObject var config: AppConfiguration = appConfiguration @@ -49,21 +50,45 @@ struct CartView: View { VStack(spacing: DesignSystem.buttonSpacing) { if let cartId = cartManager.cart?.id.rawValue { - AcceleratedCheckoutButtons(cartID: cartId) - .wallets([.shopPay, .applePay]) - .cornerRadius(DesignSystem.cornerRadius) - .onComplete { _ in - // Reset cart on successful checkout - CartManager.shared.resetCart() - } - .onFail { error in - print("Accelerated checkout failed: \(error)") + VStack { + if case .loading = acceleratedCheckoutState { + VStack(spacing: DesignSystem.buttonSpacing) { + SkeletonButton(cornerRadius: DesignSystem.cornerRadius) + SkeletonButton(cornerRadius: DesignSystem.cornerRadius) + } } - .onCancel { - print("Accelerated checkout cancelled") + + if case .error = acceleratedCheckoutState { + VStack { + Image(systemName: "exclamationmark.triangle") + .foregroundColor(.orange) + .font(.title2) + Text("Unable to load checkout buttons") + .font(.caption) + .foregroundColor(.secondary) + } + .frame(height: 44) } - .environment(appConfiguration.acceleratedCheckoutsStorefrontConfig) - .environment(appConfiguration.acceleratedCheckoutsApplePayConfig) + + AcceleratedCheckoutButtons(cartID: cartId) + .wallets([.shopPay, .applePay]) + .cornerRadius(DesignSystem.cornerRadius) + .onRenderStateChange { state in + acceleratedCheckoutState = state + } + .onComplete { _ in + // Reset cart on successful checkout + CartManager.shared.resetCart() + } + .onFail { error in + print("Accelerated checkout failed: \(error)") + } + .onCancel { + print("Accelerated checkout cancelled") + } + .environment(appConfiguration.acceleratedCheckoutsStorefrontConfig) + .environment(appConfiguration.acceleratedCheckoutsApplePayConfig) + } } Button( diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/ProductView.swift b/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/ProductView.swift index 20f264b15..da226dbc3 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/ProductView.swift +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/ProductView.swift @@ -37,6 +37,7 @@ struct ProductView: View { @State private var showingCart = false @State private var descriptionExpanded: Bool = false @State private var addedToCart: Bool = false + @State private var acceleratedCheckoutState: RenderState = .loading init(product: Storefront.Product) { _product = State(initialValue: product) @@ -131,17 +132,38 @@ struct ProductView: View { .disabled(!variant.availableForSale || loading) if variant.availableForSale { - AcceleratedCheckoutButtons(variantID: variant.id.rawValue, quantity: 1) - .wallets([.applePay]) - .cornerRadius(DesignSystem.cornerRadius) - .onFail { error in - print("Accelerated checkout failed: \(error)") + VStack { + if case .loading = acceleratedCheckoutState { + SkeletonButton(cornerRadius: DesignSystem.cornerRadius) } - .onCancel { - print("Accelerated checkout cancelled") + + if case .error = acceleratedCheckoutState { + VStack { + Image(systemName: "exclamationmark.triangle") + .foregroundColor(.orange) + .font(.title2) + Text("Unable to load checkout buttons") + .font(.caption) + .foregroundColor(.secondary) + } + .frame(height: 44) } - .environment(appConfiguration.acceleratedCheckoutsStorefrontConfig) - .environment(appConfiguration.acceleratedCheckoutsApplePayConfig) + + AcceleratedCheckoutButtons(variantID: variant.id.rawValue, quantity: 1) + .wallets([.applePay]) + .cornerRadius(DesignSystem.cornerRadius) + .onRenderStateChange { state in + acceleratedCheckoutState = state + } + .onFail { error in + print("Accelerated checkout failed: \(error)") + } + .onCancel { + print("Accelerated checkout cancelled") + } + .environment(appConfiguration.acceleratedCheckoutsStorefrontConfig) + .environment(appConfiguration.acceleratedCheckoutsApplePayConfig) + } } }.padding([.leading, .trailing], 15) } diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/SkeletonButton.swift b/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/SkeletonButton.swift new file mode 100644 index 000000000..540522436 --- /dev/null +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/Views/SkeletonButton.swift @@ -0,0 +1,74 @@ +/* + 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 SwiftUI + +struct SkeletonButton: View { + let cornerRadius: CGFloat + @State private var isAnimating = false + + init(cornerRadius: CGFloat = 8) { + self.cornerRadius = cornerRadius + } + + var body: some View { + RoundedRectangle(cornerRadius: cornerRadius) + .fill( + LinearGradient( + colors: [ + Color.gray.opacity(0.3), + Color.gray.opacity(0.1), + Color.gray.opacity(0.3) + ], + startPoint: .leading, + endPoint: .trailing + ) + ) + .frame(height: 44) + .overlay( + RoundedRectangle(cornerRadius: cornerRadius) + .fill( + LinearGradient( + colors: [ + Color.clear, + Color.white.opacity(0.4), + Color.clear + ], + startPoint: .leading, + endPoint: .trailing + ) + ) + .scaleEffect(x: 0.3, y: 1) + .offset(x: isAnimating ? 200 : -200) + .animation( + Animation.linear(duration: 1.5) + .repeatForever(autoreverses: false), + value: isAnimating + ) + ) + .clipped() + .onAppear { + isAnimating = true + } + } +} \ No newline at end of file