Skip to content

Commit d32973e

Browse files
Fix V4 Apple Pay button customization API (#321)
## What? Ports the relevant Apple Pay customization fix from the legacy V3 checkout sheet kit into the V4 Swift SDK. - Replaces the V4 Apple Pay customization API with PassKit-native modifiers: - `.applePayButtonType(_:)` - `.applePayButtonStyle(_:)` - Stores and passes `PKPaymentButtonType` / `PKPaymentButtonStyle` directly through the Apple Pay button stack. - Removes the V3-only SwiftUI wrapper conversion path from V4. - Includes the Apple Pay button type in the SwiftUI identity so dynamic type changes recreate the underlying `PKPaymentButton`. - Updates Swift samples, README usage, and the Swift API baseline. ## Why? The V3 patch needed backwards-compatible shims for deprecated Apple Pay customization APIs. V4 does not need that compatibility layer, so this keeps only the V4-relevant fix: using native PassKit values directly and ensuring the button identity reflects both style and type. ## Validation - [x] `shadowenv exec -- env CURRENT_SIMULATOR_UUID=E8D7913B-0229-4D36-9B22-455726BEDA9E /opt/dev/bin/dev swift test ApplePayButtonCustomizationTests` - [x] `shadowenv exec -- /opt/dev/bin/dev swift api check` - [x] `shadowenv exec -- /opt/dev/bin/dev swift lint` - [x] `shadowenv exec -- env CURRENT_SIMULATOR_UUID=E8D7913B-0229-4D36-9B22-455726BEDA9E /opt/dev/bin/dev swift test` - [x] `shadowenv exec -- env CURRENT_SIMULATOR_UUID=E8D7913B-0229-4D36-9B22-455726BEDA9E /opt/dev/bin/dev swift build packages` - [x] Branch-built iOS sample smoke with `agent-device`: built and installed `CheckoutKitSwiftDemo.app`, opened first product, verified enabled/hittable `Buy with Apple Pay` button rendered. Smoke screenshot saved locally at `/tmp/checkout-kit-agent-device/v4-checkoutkit-demo-apple-pay-product.png`.
1 parent 3b80776 commit d32973e

9 files changed

Lines changed: 166 additions & 102 deletions

File tree

platforms/swift/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,8 +392,8 @@ import ShopifyAcceleratedCheckouts
392392

393393
AcceleratedCheckoutButtons(cartID: cartID)
394394
.wallets([.shopPay, .applePay])
395-
.applePayLabel(.buy)
396-
.applePayStyle(.automatic)
395+
.applePayButtonType(.buy)
396+
.applePayButtonStyle(.automatic)
397397
.cornerRadius(8)
398398
.onRenderStateChange { state in
399399
// loading, rendered, or error(reason:)

platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/Cart/CartView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ struct CartView: View {
4141
if let cartID = cartManager.cart?.id {
4242
if #available(iOS 16, *) {
4343
AcceleratedCheckoutButtons(cartID: cartID)
44-
.applePayStyle(applePayStyle.style)
44+
.applePayButtonStyle(applePayStyle.style)
4545
.onFail { error in
4646
print("[AcceleratedCheckout] Failed: \(error)")
4747
}

platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/ProductView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ struct ProductView: View {
116116
if #available(iOS 16, *) {
117117
AcceleratedCheckoutButtons(variantID: variant.id, quantity: 1)
118118
.wallets([.applePay])
119-
.applePayStyle(applePayStyle.style)
119+
.applePayButtonStyle(applePayStyle.style)
120120
.onFail { error in
121121
print("[AcceleratedCheckout] Failed: \(error)")
122122
}

platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/SettingsView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ enum ApplePayStyleOption: String, CaseIterable {
328328
}
329329

330330
@available(iOS 16.0, *)
331-
var style: PayWithApplePayButtonStyle {
331+
var style: PKPaymentButtonStyle {
332332
switch self {
333333
case .automatic: return .automatic
334334
case .black: return .black

platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp/Views/Components/ButtonSet.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ struct ButtonSet: View {
1919
renderState: $cartRenderState
2020
) {
2121
AcceleratedCheckoutButtons(cartID: cartID)
22-
.applePayLabel(.plain)
22+
.applePayButtonType(.plain)
2323
.onFail { error in
2424
print("❌ Checkout failed: \(error)")
2525
}
@@ -43,8 +43,8 @@ struct ButtonSet: View {
4343
variantID: productVariant.id,
4444
quantity: firstVariantQuantity
4545
)
46-
.applePayStyle(.whiteOutline)
47-
.applePayLabel(.buy)
46+
.applePayButtonStyle(.whiteOutline)
47+
.applePayButtonType(.buy)
4848
.cornerRadius(24)
4949
.wallets([.applePay, .shopPay])
5050
.onFail { error in

platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ public struct AcceleratedCheckoutButtons: View {
2626
var cornerRadius: CGFloat?
2727
var clientContainer: CheckoutProtocolClientContainer = .init()
2828

29-
/// The Apple Pay button label style
30-
private var applePayLabel: PayWithApplePayButtonLabel = .plain
31-
private var applePayStyle: PayWithApplePayButtonStyle = .automatic
29+
/// The Apple Pay button type
30+
private var applePayButtonType: PKPaymentButtonType = .plain
31+
private var applePayButtonStyle: PKPaymentButtonStyle = .automatic
3232

3333
@State private var shopSettings: ShopSettings?
3434
@State private var currentRenderState: RenderState = .loading {
@@ -37,10 +37,9 @@ public struct AcceleratedCheckoutButtons: View {
3737
}
3838
}
3939

40-
/// Initializes an Apple Pay button with a cart ID
40+
/// Initializes accelerated checkout buttons with a cart ID
4141
/// - Parameters:
4242
/// - cartID: The cart ID to checkout (must start with gid://shopify/Cart/)
43-
/// - label: The label to display on the Apple Pay button
4443
public init(cartID: String) {
4544
identifier = .cart(cartID: cartID).parse()
4645
if case let .invariant(reason) = identifier {
@@ -49,11 +48,10 @@ public struct AcceleratedCheckoutButtons: View {
4948
}
5049
}
5150

52-
/// Initializes an Apple Pay button with a variant ID
51+
/// Initializes accelerated checkout buttons with a variant ID
5352
/// - Parameters:
5453
/// - variantID: The variant ID to checkout (must start with gid://shopify/ProductVariant/)
5554
/// - quantity: The quantity of the variant to checkout
56-
/// - label: The label to display on the Apple Pay button
5755
public init(variantID: String, quantity: Int) {
5856
identifier = .variant(variantID: variantID, quantity: quantity).parse()
5957
if case let .invariant(reason) = identifier {
@@ -73,10 +71,10 @@ public struct AcceleratedCheckoutButtons: View {
7371
identifier: identifier,
7472
eventHandlers: eventHandlers,
7573
cornerRadius: cornerRadius,
76-
style: applePayStyle,
74+
buttonType: applePayButtonType,
75+
buttonStyle: applePayButtonStyle,
7776
client: clientContainer.client
7877
)
79-
.label(applePayLabel)
8078
case .shopPay:
8179
ShopPayButton(
8280
identifier: identifier,
@@ -127,15 +125,15 @@ public struct AcceleratedCheckoutButtons: View {
127125

128126
@available(iOS 16.0, *)
129127
extension AcceleratedCheckoutButtons {
130-
public func applePayStyle(_ color: PayWithApplePayButtonStyle) -> AcceleratedCheckoutButtons {
128+
public func applePayButtonType(_ type: PKPaymentButtonType) -> AcceleratedCheckoutButtons {
131129
var view = self
132-
view.applePayStyle = color
130+
view.applePayButtonType = type
133131
return view
134132
}
135133

136-
public func applePayLabel(_ label: PayWithApplePayButtonLabel) -> AcceleratedCheckoutButtons {
134+
public func applePayButtonStyle(_ style: PKPaymentButtonStyle) -> AcceleratedCheckoutButtons {
137135
var view = self
138-
view.applePayLabel = label
136+
view.applePayButtonStyle = style
139137
return view
140138
}
141139

platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayButton.swift

Lines changed: 22 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ struct ApplePayButton: View {
2323
/// The event handlers for checkout events
2424
private let eventHandlers: EventHandlers
2525

26-
/// The Apple Pay button label style
27-
private var label: PayWithApplePayButtonLabel = .plain
26+
/// The Apple Pay button type
27+
private let buttonType: PKPaymentButtonType
2828

2929
/// The Apple Pay button style
30-
private var style: PayWithApplePayButtonStyle = .automatic
30+
private let buttonStyle: PKPaymentButtonStyle
3131

3232
/// The corner radius for the button
3333
private let cornerRadius: CGFloat?
@@ -38,13 +38,15 @@ struct ApplePayButton: View {
3838
identifier: CheckoutIdentifier,
3939
eventHandlers: EventHandlers = EventHandlers(),
4040
cornerRadius: CGFloat?,
41-
style: PayWithApplePayButtonStyle = .automatic,
41+
buttonType: PKPaymentButtonType = .plain,
42+
buttonStyle: PKPaymentButtonStyle = .automatic,
4243
client: (any CheckoutCommunicationProtocol)? = nil
4344
) {
4445
self.identifier = identifier.parse()
4546
self.eventHandlers = eventHandlers
4647
self.cornerRadius = cornerRadius
47-
self.style = style
48+
self.buttonType = buttonType
49+
self.buttonStyle = buttonStyle
4850
clientContainer = CheckoutProtocolClientContainer(client)
4951
}
5052

@@ -55,8 +57,8 @@ struct ApplePayButton: View {
5557
default:
5658
Internal_ApplePayButton(
5759
identifier: identifier,
58-
label: label,
59-
style: style,
60+
buttonType: buttonType,
61+
buttonStyle: buttonStyle,
6062
configuration: ApplePayConfigurationWrapper(
6163
common: resolvedConfiguration,
6264
applePay: resolvedApplePayConfiguration,
@@ -82,18 +84,6 @@ struct ApplePayButton: View {
8284
}
8385
return applePayConfiguration
8486
}
85-
86-
func applePayStyle(_ style: PayWithApplePayButtonStyle) -> some View {
87-
var view = self
88-
view.style = style
89-
return view
90-
}
91-
92-
func label(_ label: PayWithApplePayButtonLabel) -> some View {
93-
var view = self
94-
view.label = label
95-
return view
96-
}
9787
}
9888

9989
/// A view that displays an Apple Pay button for checkout
@@ -102,16 +92,20 @@ struct ApplePayButton: View {
10292
@available(macOS, unavailable)
10393
@MainActor
10494
struct Internal_ApplePayButton: View {
105-
private let label: PayWithApplePayButtonLabel
106-
private let style: PayWithApplePayButtonStyle
95+
private let buttonType: PKPaymentButtonType
96+
private let buttonStyle: PKPaymentButtonStyle
10797
private let controller: ApplePayViewController
10898
private let cornerRadius: CGFloat?
10999
@Environment(\.colorScheme) private var colorScheme
110100

101+
func buttonIdentity(colorScheme: ColorScheme) -> String {
102+
return "\(colorScheme)-\(buttonType.rawValue)-\(buttonStyle.rawValue)"
103+
}
104+
111105
init(
112106
identifier: CheckoutIdentifier,
113-
label: PayWithApplePayButtonLabel,
114-
style: PayWithApplePayButtonStyle,
107+
buttonType: PKPaymentButtonType,
108+
buttonStyle: PKPaymentButtonStyle,
115109
configuration: ApplePayConfigurationWrapper,
116110
eventHandlers: EventHandlers = EventHandlers(),
117111
cornerRadius: CGFloat?,
@@ -122,8 +116,8 @@ struct Internal_ApplePayButton: View {
122116
configuration: configuration,
123117
client: client
124118
)
125-
self.label = label
126-
self.style = style
119+
self.buttonType = buttonType
120+
self.buttonStyle = buttonStyle
127121
self.cornerRadius = cornerRadius
128122
controller.onCheckoutFail = eventHandlers.checkoutDidFail
129123
controller.onCheckoutCancel = eventHandlers.checkoutDidCancel
@@ -132,54 +126,15 @@ struct Internal_ApplePayButton: View {
132126
var body: some View {
133127
if PKPaymentAuthorizationController.canMakePayments() {
134128
ApplePayButtonRepresentable(
135-
buttonType: label.pkPaymentButtonType,
136-
buttonStyle: style.pkPaymentButtonStyle,
129+
buttonType: buttonType,
130+
buttonStyle: buttonStyle,
137131
cornerRadius: cornerRadius ?? 8,
138132
action: { Task { @MainActor in await controller.onPress() } }
139133
)
140-
.id("\(colorScheme)-\(style.pkPaymentButtonStyle.rawValue)")
134+
.id(buttonIdentity(colorScheme: colorScheme))
141135
.frame(height: 48)
142136
} else {
143137
Text("errors.applePay.unsupported".localizedString)
144138
}
145139
}
146140
}
147-
148-
// MARK: - Type Conversions
149-
150-
@available(iOS 16.0, *)
151-
extension PayWithApplePayButtonStyle {
152-
var pkPaymentButtonStyle: PKPaymentButtonStyle {
153-
switch self {
154-
case .black: .black
155-
case .white: .white
156-
case .whiteOutline: .whiteOutline
157-
case .automatic: .automatic
158-
default: .automatic
159-
}
160-
}
161-
}
162-
163-
@available(iOS 16.0, *)
164-
extension PayWithApplePayButtonLabel {
165-
var pkPaymentButtonType: PKPaymentButtonType {
166-
switch self {
167-
case .buy: .buy
168-
case .setUp: .setUp
169-
case .inStore: .inStore
170-
case .donate: .donate
171-
case .checkout: .checkout
172-
case .book: .book
173-
case .subscribe: .subscribe
174-
case .reload: .reload
175-
case .addMoney: .addMoney
176-
case .topUp: .topUp
177-
case .order: .order
178-
case .rent: .rent
179-
case .support: .support
180-
case .contribute: .contribute
181-
case .tip: .tip
182-
default: .plain
183-
}
184-
}
185-
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import PassKit
2+
@testable import ShopifyAcceleratedCheckouts
3+
import SwiftUI
4+
import XCTest
5+
6+
@available(iOS 16.0, *)
7+
@MainActor
8+
final class ApplePayButtonCustomizationTests: XCTestCase {
9+
func test_applePayButtonType_withPassKitButtonTypeModifier_shouldStorePassKitButtonType() {
10+
let view = AcceleratedCheckoutButtons(cartID: "gid://Shopify/Cart/test-cart-id")
11+
.applePayButtonType(.buy)
12+
13+
XCTAssertEqual(storedApplePayButtonType(in: view)?.rawValue, PKPaymentButtonType.buy.rawValue)
14+
}
15+
16+
func test_applePayButtonStyle_withPassKitButtonStyleModifier_shouldStorePassKitButtonStyle() {
17+
let view = AcceleratedCheckoutButtons(cartID: "gid://Shopify/Cart/test-cart-id")
18+
.applePayButtonStyle(.black)
19+
20+
XCTAssertEqual(storedApplePayButtonStyle(in: view)?.rawValue, PKPaymentButtonStyle.black.rawValue)
21+
}
22+
23+
func test_applePayButton_withPassKitValues_shouldPassValuesToInternalButton() {
24+
let button = ApplePayButton(
25+
identifier: .cart(cartID: "gid://Shopify/Cart/test-cart-id"),
26+
cornerRadius: nil,
27+
buttonType: .buy,
28+
buttonStyle: .whiteOutline
29+
)
30+
31+
XCTAssertEqual(storedButtonType(in: button)?.rawValue, PKPaymentButtonType.buy.rawValue)
32+
XCTAssertEqual(storedButtonStyle(in: button)?.rawValue, PKPaymentButtonStyle.whiteOutline.rawValue)
33+
}
34+
35+
func test_internalApplePayButton_withPassKitValues_shouldStoreValuesDirectly() {
36+
let button = Internal_ApplePayButton(
37+
identifier: .cart(cartID: "gid://Shopify/Cart/test-cart-id"),
38+
buttonType: .buy,
39+
buttonStyle: .whiteOutline,
40+
configuration: .testConfiguration,
41+
cornerRadius: nil
42+
)
43+
44+
XCTAssertEqual(storedButtonType(in: button)?.rawValue, PKPaymentButtonType.buy.rawValue)
45+
XCTAssertEqual(storedButtonStyle(in: button)?.rawValue, PKPaymentButtonStyle.whiteOutline.rawValue)
46+
}
47+
48+
func test_applePayButtonRepresentable_withPassKitValues_shouldStoreValuesDirectly() {
49+
let representable = ApplePayButtonRepresentable(
50+
buttonType: PKPaymentButtonType.buy,
51+
buttonStyle: PKPaymentButtonStyle.whiteOutline,
52+
cornerRadius: 8,
53+
action: {}
54+
)
55+
56+
XCTAssertEqual(storedRepresentableButtonType(in: representable)?.rawValue, PKPaymentButtonType.buy.rawValue)
57+
XCTAssertEqual(storedRepresentableButtonStyle(in: representable)?.rawValue, PKPaymentButtonStyle.whiteOutline.rawValue)
58+
}
59+
60+
func test_buttonIdentity_withDifferentButtonTypes_shouldChangeIdentity() {
61+
let plainButton = Internal_ApplePayButton(
62+
identifier: .cart(cartID: "gid://Shopify/Cart/test-cart-id"),
63+
buttonType: .plain,
64+
buttonStyle: .automatic,
65+
configuration: .testConfiguration,
66+
cornerRadius: nil
67+
)
68+
let buyButton = Internal_ApplePayButton(
69+
identifier: .cart(cartID: "gid://Shopify/Cart/test-cart-id"),
70+
buttonType: .buy,
71+
buttonStyle: .automatic,
72+
configuration: .testConfiguration,
73+
cornerRadius: nil
74+
)
75+
76+
XCTAssertNotEqual(
77+
plainButton.buttonIdentity(colorScheme: .light),
78+
buyButton.buttonIdentity(colorScheme: .light)
79+
)
80+
}
81+
82+
private func storedApplePayButtonType(in view: AcceleratedCheckoutButtons) -> PKPaymentButtonType? {
83+
return childValue(named: "applePayButtonType", in: view)
84+
}
85+
86+
private func storedApplePayButtonStyle(in view: AcceleratedCheckoutButtons) -> PKPaymentButtonStyle? {
87+
return childValue(named: "applePayButtonStyle", in: view)
88+
}
89+
90+
private func storedButtonType(in value: some Any) -> PKPaymentButtonType? {
91+
return childValue(named: "buttonType", in: value)
92+
}
93+
94+
private func storedButtonStyle(in value: some Any) -> PKPaymentButtonStyle? {
95+
return childValue(named: "buttonStyle", in: value)
96+
}
97+
98+
private func storedRepresentableButtonType(in value: some Any) -> PKPaymentButtonType? {
99+
return childValue(named: "buttonType", in: value)
100+
}
101+
102+
private func storedRepresentableButtonStyle(in value: some Any) -> PKPaymentButtonStyle? {
103+
return childValue(named: "buttonStyle", in: value)
104+
}
105+
106+
private func childValue<Value>(named name: String, in value: some Any) -> Value? {
107+
return Mirror(reflecting: value).children.first { child in
108+
child.label == name
109+
}?.value as? Value
110+
}
111+
}

0 commit comments

Comments
 (0)