From 4ef01230279c781f33e3223a62e4648ecd964b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20=C3=81lvarez?= Date: Thu, 17 Jul 2025 11:33:38 +0200 Subject: [PATCH] Accelerated Checkouts: automate Apple Pay supported networks from merchant configuration --- .../ShopifyAcceleratedCheckoutsApp.swift | 2 - .../StorefrontAPI/StorefrontAPI+Types.swift | 24 +++++++- .../ApplePay/ApplePayConfiguration.swift | 14 ++--- .../ApplePay/Data/CardBrandMapper.swift | 55 +++++++++++++++++++ .../Wallets/ApplePay/Data/PKDecoder.swift | 6 +- .../StorefrontAPI/ShopSettingsTests.swift | 2 +- .../TestHelpers.swift | 12 ++-- .../ApplePay/ApplePayCallbackTests.swift | 4 +- .../ApplePay/ApplePayIntegrationTests.swift | 7 ++- .../ApplePayViewControllerTests.swift | 4 +- .../ApplePay/ApplePayViewModifierTests.swift | 3 +- 11 files changed, 98 insertions(+), 35 deletions(-) create mode 100644 Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/CardBrandMapper.swift diff --git a/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp.swift b/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp.swift index 41437e98..413a4fb2 100644 --- a/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp.swift +++ b/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp.swift @@ -21,7 +21,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import PassKit import ShopifyAcceleratedCheckouts import SwiftUI @@ -78,7 +77,6 @@ private func createApplePayConfiguration( return ShopifyAcceleratedCheckouts.ApplePayConfiguration( merchantIdentifier: "merchant.com.shopify.example.ShopifyAcceleratedCheckoutsApp", - supportedNetworks: [.amex, .discover, .masterCard, .visa], contactFields: fields ) } diff --git a/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift b/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift index 99144719..3047c18f 100644 --- a/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift +++ b/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift @@ -37,6 +37,7 @@ extension StorefrontAPI { typealias Money = StorefrontAPI.MoneyV2 typealias Address = StorefrontAPI.Address typealias ApplePayPayment = StorefrontAPI.ApplePayPayment + typealias CardBrand = StorefrontAPI.CardBrand } /// Represents a cart in the Storefront API @@ -353,10 +354,20 @@ extension StorefrontAPI { /// Shop payment settings struct ShopPaymentSettings: Codable { let supportedDigitalWallets: [String] - let acceptedCardBrands: [String] + let acceptedCardBrands: [CardBrand] let countryCode: String } + /// Card brands supported by Shopify's payment system + enum CardBrand: String, Codable, CaseIterable { + case americanExpress = "AMERICAN_EXPRESS" + case dinersClub = "DINERS_CLUB" + case discover = "DISCOVER" + case jcb = "JCB" + case mastercard = "MASTERCARD" + case visa = "VISA" + } + // MARK: - Delivery Groups /// Connection type for delivery groups @@ -1010,7 +1021,8 @@ extension StorefrontAPI.Address { shop.paymentSettings.countryCode let paymentSettings = PaymentSettings( - countryCode: countryCode + countryCode: countryCode, + acceptedCardBrands: shop.paymentSettings.acceptedCardBrands ) // Extract primary domain @@ -1028,16 +1040,22 @@ extension StorefrontAPI.Address { } /// Payment settings for the shop +@available(iOS 17.0, *) class PaymentSettings { /// The shop's country code (e.g., "US", "CA") let countryCode: String - init(countryCode: String) { + /// Card brands accepted by the merchant + let acceptedCardBrands: [StorefrontAPI.CardBrand] + + init(countryCode: String, acceptedCardBrands: [StorefrontAPI.CardBrand] = []) { self.countryCode = countryCode + self.acceptedCardBrands = acceptedCardBrands } } /// Domain information for the shop +@available(iOS 17.0, *) class Domain { /// The host name of the domain (e.g., "example.myshopify.com") let host: String diff --git a/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayConfiguration.swift b/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayConfiguration.swift index 0583f432..208fc562 100644 --- a/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayConfiguration.swift +++ b/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayConfiguration.swift @@ -34,7 +34,8 @@ extension ShopifyAcceleratedCheckouts { /// Configuration options for Apple Pay integration within Shopify Accelerated Checkouts. /// /// This class encapsulates all necessary settings for enabling Apple Pay as a payment method, - /// including merchant identification, supported payment networks, and required contact information. + /// including merchant identification and required contact information. Supported payment networks + /// are automatically determined based on the merchant's Shopify configuration. @Observable public class ApplePayConfiguration { /// The merchant identifier for Apple Pay transactions. /// @@ -44,12 +45,6 @@ extension ShopifyAcceleratedCheckouts { /// - See: [Apple Developer Documentation - merchantIdentifier](https://developer.apple.com/documentation/passkit_apple_pay_and_wallet/pkpaymentrequest/1619305-merchantidentifier) public let merchantIdentifier: String - /// Payment card networks supported for Apple Pay transactions. - /// - /// Only card types included in this array will be displayed as available payment - /// options in the Apple Pay payment sheet. - public let supportedNetworks: [PKPaymentNetwork] - /// Contact information fields required during the Apple Pay payment flow. /// /// Fields specified in this array will be marked as required in the payment sheet. @@ -65,15 +60,14 @@ extension ShopifyAcceleratedCheckouts { /// /// - Parameters: /// - merchantIdentifier: The merchant identifier registered with Apple. - /// - supportedNetworks: Array of payment card networks to accept. /// - contactFields: Contact information fields to require from the customer. + /// - Note: Supported payment networks are automatically determined based on the + /// merchant's accepted card brands configuration in Shopify. public init( merchantIdentifier: String, - supportedNetworks: [PKPaymentNetwork], contactFields: [RequiredContactFields] ) { self.merchantIdentifier = merchantIdentifier - self.supportedNetworks = supportedNetworks self.contactFields = contactFields } } diff --git a/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/CardBrandMapper.swift b/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/CardBrandMapper.swift new file mode 100644 index 00000000..daaee506 --- /dev/null +++ b/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/CardBrandMapper.swift @@ -0,0 +1,55 @@ +/* + 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 PassKit + +@available(iOS 17.0, *) +enum CardBrandMapper { + /// Maps Shopify's CardBrand enum values to Apple Pay's PKPaymentNetwork values + /// - Parameter shopifyCardBrand: The card brand from Shopify's acceptedCardBrands + /// - Returns: The corresponding PKPaymentNetwork, or nil if the brand is not supported by Apple Pay + static func mapToPKPaymentNetwork(_ shopifyCardBrand: StorefrontAPI.CardBrand) -> PKPaymentNetwork? { + switch shopifyCardBrand { + case .americanExpress: + return .amex + case .discover: + return .discover + case .jcb: + return .JCB + case .mastercard: + return .masterCard + case .visa: + return .visa + case .dinersClub: + // Diners Club is not supported by Apple Pay + return nil + } + } + + /// Maps an array of Shopify card brands to PKPaymentNetwork values + /// - Parameter shopifyCardBrands: Array of card brands from Shopify + /// - Returns: Array of PKPaymentNetwork values, filtering out any unsupported brands + static func mapToPKPaymentNetworks(_ shopifyCardBrands: [StorefrontAPI.CardBrand]) -> [PKPaymentNetwork] { + shopifyCardBrands.compactMap { mapToPKPaymentNetwork($0) } + } +} diff --git a/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/PKDecoder.swift b/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/PKDecoder.swift index 5ca54af2..19459d6a 100644 --- a/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/PKDecoder.swift +++ b/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/PKDecoder.swift @@ -49,7 +49,11 @@ class PKDecoder { let currencyCode = cart.cost.totalAmount.currencyCode paymentRequest.merchantIdentifier = configuration.applePay.merchantIdentifier - paymentRequest.supportedNetworks = configuration.applePay.supportedNetworks + + // Map accepted card brands from Shopify to PKPaymentNetwork + let acceptedCardBrands = configuration.shopSettings.paymentSettings.acceptedCardBrands + paymentRequest.supportedNetworks = CardBrandMapper.mapToPKPaymentNetworks(acceptedCardBrands) + paymentRequest.countryCode = configuration.shopSettings.paymentSettings.countryCode paymentRequest.currencyCode = currencyCode initialCurrencyCode = currencyCode diff --git a/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/ShopSettingsTests.swift b/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/ShopSettingsTests.swift index 49e96fbd..bae140f3 100644 --- a/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/ShopSettingsTests.swift +++ b/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/ShopSettingsTests.swift @@ -107,7 +107,7 @@ class ShopSettingsTests: XCTestCase { shipsToCountries: ["US", "CA"], paymentSettings: StorefrontAPI.ShopPaymentSettings( supportedDigitalWallets: ["APPLE_PAY", "SHOP_PAY"], - acceptedCardBrands: ["VISA", "MASTERCARD"], + acceptedCardBrands: [.visa, .mastercard], countryCode: countryCode ), moneyFormat: "${{amount}}" diff --git a/Tests/ShopifyAcceleratedCheckoutsTests/TestHelpers.swift b/Tests/ShopifyAcceleratedCheckoutsTests/TestHelpers.swift index 7837c781..8f9d8318 100644 --- a/Tests/ShopifyAcceleratedCheckoutsTests/TestHelpers.swift +++ b/Tests/ShopifyAcceleratedCheckoutsTests/TestHelpers.swift @@ -21,7 +21,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import PassKit @testable import ShopifyAcceleratedCheckouts // MARK: - Configuration Helpers @@ -70,7 +69,8 @@ extension ShopSettings { url: "https://test-shop.myshopify.com" ), paymentSettings: PaymentSettings( - countryCode: "US" + countryCode: "US", + acceptedCardBrands: [.visa, .mastercard, .americanExpress, .discover] ) ) } @@ -82,7 +82,8 @@ extension ShopSettings { url: "https://test-shop.myshopify.com" ), paymentSettings: PaymentSettings = PaymentSettings( - countryCode: "US" + countryCode: "US", + acceptedCardBrands: [.visa, .mastercard, .americanExpress, .discover] ) ) -> ShopSettings { return ShopSettings( @@ -98,18 +99,15 @@ extension ShopifyAcceleratedCheckouts.ApplePayConfiguration { static var testConfiguration: ShopifyAcceleratedCheckouts.ApplePayConfiguration { return ShopifyAcceleratedCheckouts.ApplePayConfiguration( merchantIdentifier: "merchant.test.id", - supportedNetworks: [.visa], contactFields: [.email, .phone] ) } static func testConfiguration( - merchantIdentifier: String = "merchant.test.id", - supportedNetworks: [PKPaymentNetwork] = [.visa] + merchantIdentifier: String = "merchant.test.id" ) -> ShopifyAcceleratedCheckouts.ApplePayConfiguration { return ShopifyAcceleratedCheckouts.ApplePayConfiguration( merchantIdentifier: merchantIdentifier, - supportedNetworks: supportedNetworks, contactFields: [.email, .phone] ) } diff --git a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayCallbackTests.swift b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayCallbackTests.swift index 44600d64..42ee5faf 100644 --- a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayCallbackTests.swift +++ b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayCallbackTests.swift @@ -21,7 +21,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import PassKit @testable import ShopifyAcceleratedCheckouts @testable import ShopifyCheckoutSheetKit import XCTest @@ -50,7 +49,6 @@ final class ApplePayCallbackTests: XCTestCase { let applePayConfig = ShopifyAcceleratedCheckouts.ApplePayConfiguration( merchantIdentifier: "test.merchant.id", - supportedNetworks: [.visa, .masterCard], contactFields: [] ) @@ -60,7 +58,7 @@ final class ApplePayCallbackTests: XCTestCase { host: "test-shop.myshopify.com", url: "https://test-shop.myshopify.com" ), - paymentSettings: PaymentSettings(countryCode: "US") + paymentSettings: PaymentSettings(countryCode: "US", acceptedCardBrands: [.visa, .mastercard]) ) mockConfiguration = ApplePayConfigurationWrapper( diff --git a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayIntegrationTests.swift b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayIntegrationTests.swift index 1fc44bfa..6d7dc975 100644 --- a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayIntegrationTests.swift +++ b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayIntegrationTests.swift @@ -21,7 +21,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import PassKit @testable import ShopifyAcceleratedCheckouts import ShopifyCheckoutSheetKit import SwiftUI @@ -48,7 +47,6 @@ final class ApplePayIntegrationTests: XCTestCase { mockApplePayConfiguration = ShopifyAcceleratedCheckouts.ApplePayConfiguration( merchantIdentifier: "test.merchant.id", - supportedNetworks: [.visa, .masterCard, .amex], contactFields: [] ) @@ -58,7 +56,10 @@ final class ApplePayIntegrationTests: XCTestCase { host: "test-shop.myshopify.com", url: "https://test-shop.myshopify.com" ), - paymentSettings: PaymentSettings(countryCode: "US") + paymentSettings: PaymentSettings( + countryCode: "US", + acceptedCardBrands: [.visa, .mastercard, .americanExpress] + ) ) mockConfiguration = ApplePayConfigurationWrapper( diff --git a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayViewControllerTests.swift b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayViewControllerTests.swift index bb1937b9..e307a36f 100644 --- a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayViewControllerTests.swift +++ b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayViewControllerTests.swift @@ -21,7 +21,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import PassKit @testable import ShopifyAcceleratedCheckouts import ShopifyCheckoutSheetKit import UIKit @@ -36,7 +35,7 @@ class ApplePayViewControllerTests: XCTestCase { super.setUp() // Create mock shop settings - let paymentSettings = PaymentSettings(countryCode: "US") + let paymentSettings = PaymentSettings(countryCode: "US", acceptedCardBrands: [.visa, .mastercard]) let primaryDomain = Domain(host: "test-shop.myshopify.com", url: "https://test-shop.myshopify.com") let shopSettings = ShopSettings( name: "Test Shop", @@ -53,7 +52,6 @@ class ApplePayViewControllerTests: XCTestCase { // Create Apple Pay configuration let applePayConfig = ShopifyAcceleratedCheckouts.ApplePayConfiguration( merchantIdentifier: "test.merchant", - supportedNetworks: [.visa, .masterCard], contactFields: [] ) diff --git a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayViewModifierTests.swift b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayViewModifierTests.swift index 6eb12730..030db6a8 100644 --- a/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayViewModifierTests.swift +++ b/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayViewModifierTests.swift @@ -46,7 +46,6 @@ final class ApplePayViewModifierTests: XCTestCase { mockApplePayConfiguration = ShopifyAcceleratedCheckouts.ApplePayConfiguration( merchantIdentifier: "test.merchant.id", - supportedNetworks: [.visa, .masterCard], contactFields: [] ) @@ -56,7 +55,7 @@ final class ApplePayViewModifierTests: XCTestCase { host: "test-shop.myshopify.com", url: "https://test-shop.myshopify.com" ), - paymentSettings: PaymentSettings(countryCode: "US") + paymentSettings: PaymentSettings(countryCode: "US", acceptedCardBrands: [.visa, .mastercard]) ) }