diff --git a/CheckoutPaymentMethodsPackage/DummyPaymentMethod.swift b/CheckoutPaymentMethodsPackage/DummyPaymentMethod.swift new file mode 100644 index 0000000..e69de29 diff --git a/Package.swift b/Package.swift index 04d35dd..88f435f 100644 --- a/Package.swift +++ b/Package.swift @@ -1,12 +1,14 @@ // swift-tools-version: 5.10 import PackageDescription -let releaseVersion = "1.10.0" +let releaseVersion = "2.0.0" let githubRepo = "checkout/checkout-ios-components" -let sdkChecksum = "d9b4f08daa7f1d75032675a6e21b1c899d72f5e0bcc1fa2a25ec2f609039cbf7" +let sdkChecksum = "4a5f41fa4f516e58fd25b67488a576d2e4544fea68f8eb0b228e6dac3386ed8a" +let paymentMethodsChecksum = "9edfb84e39cb35f80c5e81ce6030cc4dce62ca0f3c36a970b89ce52a2b33035d" let sdkURL = "https://github.com/\(githubRepo)/releases/download/\(releaseVersion)/CheckoutComponentsSDK.xcframework.zip" +let paymentMethodsURL = "https://github.com/\(githubRepo)/releases/download/\(releaseVersion)/CheckoutPaymentMethods.xcframework.zip" let package = Package( name: "CheckoutComponents", @@ -17,9 +19,11 @@ let package = Package( products: [ .library( name: "CheckoutComponents", - targets: [ - "CheckoutComponentsPackage" - ] + targets: ["CheckoutComponentsPackage"] + ), + .library( + name: "CheckoutPaymentMethods", + targets: ["CheckoutPaymentMethodsPackage"] ), ], dependencies: [ @@ -41,6 +45,19 @@ let package = Package( name: "CheckoutComponentsSDK", url: sdkURL, checksum: sdkChecksum - ) + ), + .target( + name: "CheckoutPaymentMethodsPackage", + dependencies: [ + .target(name: "CheckoutPaymentMethods"), + .target(name: "CheckoutComponentsPackage"), + ], + path: "CheckoutPaymentMethodsPackage" + ), + .binaryTarget( + name: "CheckoutPaymentMethods", + url: paymentMethodsURL, + checksum: paymentMethodsChecksum + ), ] -) +) \ No newline at end of file diff --git a/SampleApplication/SampleApplication.xcodeproj/project.pbxproj b/SampleApplication/SampleApplication.xcodeproj/project.pbxproj index 3927146..b391191 100644 --- a/SampleApplication/SampleApplication.xcodeproj/project.pbxproj +++ b/SampleApplication/SampleApplication.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ @@ -29,11 +29,12 @@ A246560F2F44CE66001AE99A /* CheckoutComponents in Frameworks */ = {isa = PBXBuildFile; productRef = A246560E2F44CE66001AE99A /* CheckoutComponents */; }; A26B7BEB2E27CD1200B1E840 /* SubmitPaymentSessionRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A26B7BEA2E27CD1200B1E840 /* SubmitPaymentSessionRequest.swift */; }; A27312D42F475793006F7FDE /* Risk in Frameworks */ = {isa = PBXBuildFile; productRef = 16A5BCA82D835918007B79C0 /* Risk */; }; + A2D1C7832FBCD82C0085BAA0 /* MainViewModel+CountryCurrencyOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2D1C7822FBCD82C0085BAA0 /* MainViewModel+CountryCurrencyOption.swift */; }; A2EF10AD2F22771700CDC478 /* MainViewModel+LocaleOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2EF10AC2F22771700CDC478 /* MainViewModel+LocaleOption.swift */; }; - FFE7091D2FA89D2E0029DBD6 /* CheckoutComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 1695611B2D440E3E00030923 /* CheckoutComponents */; }; - FFE7091E2FA89D2E0029DBD6 /* CheckoutComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 951AE3372D5243E800519900 /* CheckoutComponents */; }; - FFE7091F2FA89D2E0029DBD6 /* CheckoutComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 951AE33A2D527A7400519900 /* CheckoutComponents */; }; - FFE709202FA89D2E0029DBD6 /* CheckoutComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 16A28E2B2E71769F003668B9 /* CheckoutComponents */; }; + FF2F6B0F2FC44C3C0089E837 /* CheckoutComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 1695611B2D440E3E00030923 /* CheckoutComponents */; }; + FF2F6B102FC44C3C0089E837 /* CheckoutComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 951AE3372D5243E800519900 /* CheckoutComponents */; }; + FF2F6B112FC44C3C0089E837 /* CheckoutComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 951AE33A2D527A7400519900 /* CheckoutComponents */; }; + FF2F6B122FC44C3C0089E837 /* CheckoutComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 16A28E2B2E71769F003668B9 /* CheckoutComponents */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -60,6 +61,7 @@ A21EBD672DFAD9ED00F9BC14 /* MainView+SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainView+SettingsView.swift"; sourceTree = ""; }; A21EBD692E005C2D00F9BC14 /* AddressComponentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressComponentConfiguration.swift; sourceTree = ""; }; A26B7BEA2E27CD1200B1E840 /* SubmitPaymentSessionRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmitPaymentSessionRequest.swift; sourceTree = ""; }; + A2D1C7822FBCD82C0085BAA0 /* MainViewModel+CountryCurrencyOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainViewModel+CountryCurrencyOption.swift"; sourceTree = ""; }; A2EF10AC2F22771700CDC478 /* MainViewModel+LocaleOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainViewModel+LocaleOption.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -68,10 +70,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - FFE709202FA89D2E0029DBD6 /* CheckoutComponents in Frameworks */, - FFE7091F2FA89D2E0029DBD6 /* CheckoutComponents in Frameworks */, - FFE7091E2FA89D2E0029DBD6 /* CheckoutComponents in Frameworks */, - FFE7091D2FA89D2E0029DBD6 /* CheckoutComponents in Frameworks */, + FF2F6B122FC44C3C0089E837 /* CheckoutComponents in Frameworks */, + FF2F6B112FC44C3C0089E837 /* CheckoutComponents in Frameworks */, + FF2F6B102FC44C3C0089E837 /* CheckoutComponents in Frameworks */, + FF2F6B0F2FC44C3C0089E837 /* CheckoutComponents in Frameworks */, A27312D42F475793006F7FDE /* Risk in Frameworks */, A246560F2F44CE66001AE99A /* CheckoutComponents in Frameworks */, ); @@ -113,6 +115,7 @@ isa = PBXGroup; children = ( 16FC05412C40170E00D75022 /* SampleApplication */, + FF7FD2452FD1CD3600926AD1 /* Frameworks */, 16FC05402C40170E00D75022 /* Products */, ); sourceTree = ""; @@ -182,6 +185,7 @@ 959FEF5C2C805CE1001D7C20 /* ViewModel */ = { isa = PBXGroup; children = ( + A2D1C7822FBCD82C0085BAA0 /* MainViewModel+CountryCurrencyOption.swift */, 95EC13972CB446A200812B24 /* MainViewModel+CallbacksProvider.swift */, 16EFA2382C650D3A00157450 /* MainViewModel.swift */, A2EF10AC2F22771700CDC478 /* MainViewModel+LocaleOption.swift */, @@ -198,6 +202,13 @@ path = Helpers; sourceTree = ""; }; + FF7FD2452FD1CD3600926AD1 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -252,7 +263,6 @@ ); mainGroup = 16FC05362C40170E00D75022; packageReferences = ( - A246560D2F44CE66001AE99A /* XCLocalSwiftPackageReference "../../checkout-ios-components" */, ); productRefGroup = 16FC05402C40170E00D75022 /* Products */; projectDirPath = ""; @@ -287,6 +297,7 @@ A21EBD682DFAD9ED00F9BC14 /* MainView+SettingsView.swift in Sources */, 95EC13982CB446A200812B24 /* MainViewModel+CallbacksProvider.swift in Sources */, 1699DC712C74F5500069008D /* SampleApplication.swift in Sources */, + A2D1C7832FBCD82C0085BAA0 /* MainViewModel+CountryCurrencyOption.swift in Sources */, 9513C3242C47FC0C0018BB7E /* MainView.swift in Sources */, A21EBD622DF8007900F9BC14 /* AccessibilityIdentifiers.swift in Sources */, 16C44C732C57B95500CE0CCE /* EnvironmentVars.generated.swift in Sources */, @@ -476,8 +487,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = SampleApplication/SampleApplication.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "Apple Distribution"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_ASSET_PATHS = "\"SampleApplication/Preview Content\""; DEVELOPMENT_TEAM = E32XBQK4Q5; @@ -533,13 +544,6 @@ }; /* End XCConfigurationList section */ -/* Begin XCLocalSwiftPackageReference section */ - A246560D2F44CE66001AE99A /* XCLocalSwiftPackageReference "../../checkout-ios-components" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = "../../checkout-ios-components"; - }; -/* End XCLocalSwiftPackageReference section */ - /* Begin XCSwiftPackageProductDependency section */ 1695611B2D440E3E00030923 /* CheckoutComponents */ = { isa = XCSwiftPackageProductDependency; diff --git a/SampleApplication/SampleApplication/Configuration/EnvironmentVars.stencil b/SampleApplication/SampleApplication/Configuration/EnvironmentVars.stencil index d5ff7ea..21423e1 100644 --- a/SampleApplication/SampleApplication/Configuration/EnvironmentVars.stencil +++ b/SampleApplication/SampleApplication/Configuration/EnvironmentVars.stencil @@ -1,10 +1,8 @@ //swiftlint:disable all struct EnvironmentVars { - /// Generated using 'EnvironmentVars.stencil' template - static let sandboxPublicKey = "{{ argument.COMPONENTS_SANDBOX_PUBLIC_KEY }}" - static let sandboxSecretKey = "{{ argument.COMPONENTS_SANDBOX_SECRET_KEY }}" - static let sandboxProcessingChannelID = "{{ argument.COMPONENTS_SANDBOX_PROCESSING_CHANNEL_ID }}" - - static let productionPublicKey = "{{ argument.COMPONENTS_PRODUCTION_PUBLIC_KEY }}" - static let productionSecretKey = "{{ argument.COMPONENTS_PRODUCTION_SECRET_KEY }}" + static let sandboxPublicKey = "${COMPONENTS_SANDBOX_PUBLIC_KEY}" + static let sandboxSecretKey = "${COMPONENTS_SANDBOX_SECRET_KEY}" + static let sandboxProcessingChannelID = "${COMPONENTS_SANDBOX_PROCESSING_CHANNEL_ID}" + static let productionPublicKey = "${COMPONENTS_PRODUCTION_PUBLIC_KEY}" + static let productionSecretKey = "${COMPONENTS_PRODUCTION_SECRET_KEY}" } diff --git a/SampleApplication/SampleApplication/Helpers/AccessibilityIdentifiers.swift b/SampleApplication/SampleApplication/Helpers/AccessibilityIdentifiers.swift index f0d80d2..e1aa823 100644 --- a/SampleApplication/SampleApplication/Helpers/AccessibilityIdentifiers.swift +++ b/SampleApplication/SampleApplication/Helpers/AccessibilityIdentifiers.swift @@ -17,6 +17,8 @@ enum AccessibilityIdentifier { case productionEnvironmentOption = "production_environment_option" case localePicker = "locale_picker" case paymentSessionLocalePicker = "payment_session_locale_picker" + case countryPicker = "country_picker" + case currencyPicker = "currency_picker" case customLocale = "custom_locale_option" case addressPicker = "address_picker" case applePayTypePicker = "applepay_type_picker" @@ -25,6 +27,8 @@ enum AccessibilityIdentifier { case paymentMethodPicker = "payment_method_picker" case cardPaymentMethodOption = "card_payment_method_option" case applePayPaymentMethodOption = "google_apple_pay_payment_method_option" + case tabbyPaymentMethodOption = "tabby_payment_method_option" + case tamaraPaymentMethodOption = "tamara_payment_method_option" case payButtonPicker = "pay_button_picker" case payment = "payment" case tokenize = "tokenize" @@ -35,28 +39,35 @@ enum AccessibilityIdentifier { case acceptedCardTypesPicker = "accepted_card_types_picker" case advancedFeaturesExpandable = "advanced_features" case cardholderNameMaxLengthInput = "cardholder_name_max_length_input" - + + // Payment Session + case paymentSessionConfigurationsExpandable = "ps_configurations_button" + case paymentSessionUsernameTextField = "ps_username_textfield" + case paymentSessionUserEmailTextField = "ps_user_email_textfield" + case paymentSessionUserCountryCodeTextField = "ps_user_country_code_textfield" + case paymentSessionUserPhoneNumberTextField = "ps_user_phone_number_textfield" + // RemeberMe case rememberMeConfigurationsExpandable = "remember_me_configurations" case showRememberMeToggle = "show_remember_me_toggle" case showRememberMePayButtonToggle = "show_remember_me_pay_button_toggle" case ignoreRememberMeEmailFeatureFlagToggle = "ignore_remember_me_email_feature_flag_toggle" - + // SDK RememberMe config case rememberMeSDKSetupExpandable = "remember_me_sdk_setup" case userEmailTextField = "user_email_text_field" case userCountryCodeTextField = "user_country_code_text_field" case userPhoneNumberTextField = "user_phone_number_text_field" - + // RememberMe Payment Session / Customer config case rememberMePaymentSessionSetupExpandable = "remember_me_payment_session_setup" case customerEmailInput = "customer_email_input" case customerPhoneCountryCodePicker = "customer_phone_country_code_picker" case customerPhoneNumberInput = "customer_phone_number_input" - } enum PaymentResultView: String { + case closeButton = "payment_result_close_button" case paymentIDLabel = "payment_id_label" case generatedTokenLabel = "generated_token_label" } diff --git a/SampleApplication/SampleApplication/Main/View/MainView+SettingsView.swift b/SampleApplication/SampleApplication/Main/View/MainView+SettingsView.swift index 3659b5a..7277165 100644 --- a/SampleApplication/SampleApplication/Main/View/MainView+SettingsView.swift +++ b/SampleApplication/SampleApplication/Main/View/MainView+SettingsView.swift @@ -12,6 +12,8 @@ enum CheckoutComponent: String, CaseIterable { case flow = "Flow" case card = "Card" case applePay = "Apple Pay" + case tabby = "Tabby" + case tamara = "Tamara" var accessibilityIdentifier: String { switch self { @@ -21,6 +23,10 @@ enum CheckoutComponent: String, CaseIterable { return "card" case .applePay: return "google_apple_pay" + case .tabby: + return "tabby" + case .tamara: + return "tamara" } } } @@ -39,8 +45,11 @@ extension MainView { appearanceView localeView paymentSessionLocaleView + countryView + currencyView advancedFeaturesView + paymentSessionConfigurationView rememberMeConfigurationsView } .padding(.horizontal) @@ -67,6 +76,10 @@ extension MainView { .accessibilityIdentifier(AccessibilityIdentifier.SettingsView.cardPaymentMethodOption.rawValue) Toggle("Apple Pay", isOn: $viewModel.isApplePaySelected) .accessibilityIdentifier(AccessibilityIdentifier.SettingsView.applePayPaymentMethodOption.rawValue) + Toggle("Tabby", isOn: $viewModel.isTabbySelected) + .accessibilityIdentifier(AccessibilityIdentifier.SettingsView.tabbyPaymentMethodOption.rawValue) + Toggle("Tamara", isOn: $viewModel.isTamaraSelected) + .accessibilityIdentifier(AccessibilityIdentifier.SettingsView.tamaraPaymentMethodOption.rawValue) }.accessibilityIdentifier(AccessibilityIdentifier.SettingsView.paymentMethodPicker.rawValue) } } @@ -147,6 +160,36 @@ extension MainView { } } + var countryView: some View { + HStack { + Text("Country:") + + Picker("Country", selection: $viewModel.selectedCountry) { + ForEach(CountryOption.allCases, id: \.self) { option in + Text(option.displayName) + .tag(option) + .accessibilityIdentifier(option.accessibilityIdentifier) + } + } + .accessibilityIdentifier(AccessibilityIdentifier.SettingsView.countryPicker.rawValue) + } + } + + var currencyView: some View { + HStack { + Text("Currency:") + + Picker("Currency", selection: $viewModel.selectedCurrency) { + ForEach(CurrencyOption.allCases, id: \.self) { option in + Text(option.displayName) + .tag(option) + .accessibilityIdentifier(option.accessibilityIdentifier) + } + } + .accessibilityIdentifier(AccessibilityIdentifier.SettingsView.currencyPicker.rawValue) + } + } + var environmentView: some View { HStack { Text("Environment:") @@ -216,6 +259,19 @@ extension MainView { } } + var paymentSessionConfigurationView: some View { + expandableSection(title: "Payment session Configurations", + isExpanded: $viewModel.isPaymentSessionConfigurationExpanded, + accessibilityIdentifier: AccessibilityIdentifier.SettingsView.paymentSessionConfigurationsExpandable.rawValue) { + VStack(alignment: .leading, spacing: 12) { + paymentSessionUsername + paymentSessionUserEmail + } + .padding(.leading, 16) + .transition(.opacity.combined(with: .slide)) + } + } + var rememberMeConfigurationsView: some View { expandableSection(title: "RememberMe Configurations", isExpanded: $viewModel.isRememberMeExpanded, @@ -246,6 +302,24 @@ extension MainView { } } + var paymentSessionUsername: some View { + HStack { + Text("Customer name: ") + TextField("Customer name", text: $viewModel.paymentSessionUsername) + .accessibilityIdentifier(AccessibilityIdentifier.SettingsView.paymentSessionUsernameTextField.rawValue) + .keyboardType(.namePhonePad) + } + } + + var paymentSessionUserEmail: some View { + HStack { + Text("Customer email: ") + TextField("Customer email", text: $viewModel.paymentSessionUserEmail) + .accessibilityIdentifier(AccessibilityIdentifier.SettingsView.paymentSessionUserEmailTextField.rawValue) + .keyboardType(.emailAddress) + } + } + var customButtonOperationView: some View { HStack { Text("Custom button type:") diff --git a/SampleApplication/SampleApplication/Main/View/MainView.swift b/SampleApplication/SampleApplication/Main/View/MainView.swift index d2b39e0..b8ab036 100644 --- a/SampleApplication/SampleApplication/Main/View/MainView.swift +++ b/SampleApplication/SampleApplication/Main/View/MainView.swift @@ -8,10 +8,11 @@ import CheckoutComponentsSDK import SwiftUI -enum MainViewState { +enum MainViewState: Hashable { case initial case component case settings + case error(String) } struct MainView: View { @@ -41,6 +42,9 @@ struct MainView: View { makeComponentView() case .settings: settingView + case .error(let errorMessage): + Text("An error occurred: \(errorMessage)") + .foregroundColor(.red) } } .padding() @@ -109,8 +113,12 @@ extension MainView { Button(action: { Task { if viewState == .component { viewModel.resetToDefaultConfiguration() } - await viewModel.makeComponent() - viewState = .component + do { + try await viewModel.makeComponent() + viewState = .component + } catch { + viewState = .error("\(error)") + } } }) { Text("Show Flow") diff --git a/SampleApplication/SampleApplication/Main/View/PaymentResultView.swift b/SampleApplication/SampleApplication/Main/View/PaymentResultView.swift index e909a90..692a8c7 100644 --- a/SampleApplication/SampleApplication/Main/View/PaymentResultView.swift +++ b/SampleApplication/SampleApplication/Main/View/PaymentResultView.swift @@ -78,6 +78,7 @@ struct PaymentResultView: View { presentationMode.wrappedValue.dismiss() }) { Text("Close") + .accessibilityIdentifier(AccessibilityIdentifier.PaymentResultView.closeButton.rawValue) .padding() .background(Color.blue) .foregroundColor(.white) diff --git a/SampleApplication/SampleApplication/Main/ViewModel/MainViewModel+CountryCurrencyOption.swift b/SampleApplication/SampleApplication/Main/ViewModel/MainViewModel+CountryCurrencyOption.swift new file mode 100644 index 0000000..5ae4c56 --- /dev/null +++ b/SampleApplication/SampleApplication/Main/ViewModel/MainViewModel+CountryCurrencyOption.swift @@ -0,0 +1,27 @@ +// Copyright © 2026 Checkout.com. All rights reserved. + +import Foundation + +enum CountryOption: String, CaseIterable, Hashable { + case gb = "GB" + case ae = "AE" + case sa = "SA" + + var displayName: String { rawValue } + + var accessibilityIdentifier: String { + "country_option_\(rawValue.lowercased())" + } +} + +enum CurrencyOption: String, CaseIterable, Hashable { + case gbp = "GBP" + case aed = "AED" + case sar = "SAR" + + var displayName: String { rawValue } + + var accessibilityIdentifier: String { + "currency_option_\(rawValue.lowercased())" + } +} diff --git a/SampleApplication/SampleApplication/Main/ViewModel/MainViewModel.swift b/SampleApplication/SampleApplication/Main/ViewModel/MainViewModel.swift index 84f087a..f2a5636 100644 --- a/SampleApplication/SampleApplication/Main/ViewModel/MainViewModel.swift +++ b/SampleApplication/SampleApplication/Main/ViewModel/MainViewModel.swift @@ -8,9 +8,15 @@ import CheckoutComponentsSDK import SwiftUI +#if canImport(CheckoutPaymentMethods) +import CheckoutPaymentMethods +#endif + enum PaymentMethodType: CaseIterable { case card case applePay + case tabby + case tamara } enum CustomButtonOperation: String, CaseIterable { @@ -33,6 +39,8 @@ final class MainViewModel: ObservableObject { @Published var selectedPaymentMethodTypes: Set = [] @Published var selectedLocale: LocaleOption = .locale(.en_GB) @Published var paymentSessionSelectedLocale: LocaleOption = .locale(.en_GB) + @Published var selectedCountry: CountryOption = .gb + @Published var selectedCurrency: CurrencyOption = .gbp @Published var selectedEnvironment: CheckoutComponents.Environment = .sandbox @Published var selectedAddressConfiguration: AddressComponentConfiguration = .prefillCustomized @Published var selectedApplePayType: ApplePayType = .final @@ -49,7 +57,11 @@ final class MainViewModel: ObservableObject { @Published var showApplePayButton: Bool = true @Published var isAdvancedFeaturesExpanded: Bool = false @Published var customButtonOperation: CustomButtonOperation = .submitPayment - + + @Published var isPaymentSessionConfigurationExpanded: Bool = false + @Published var paymentSessionUsername: String = "" + @Published var paymentSessionUserEmail: String = "" + // RememberMe @Published var isRememberMeExpanded: Bool = false @Published var isRememberMeSDKSetupExpanded: Bool = true @@ -87,14 +99,29 @@ final class MainViewModel: ObservableObject { var createdCheckoutComponentsSDK: CheckoutComponents? private var component: Any? private let networkLayer = NetworkLayer() - + + var apmProviders: [any CheckoutComponents.PaymentMethodProvider] { + #if canImport(CheckoutPaymentMethods) + var providers: [any CheckoutComponents.PaymentMethodProvider] = [] + if selectedPaymentMethodTypes.contains(.tabby) { + providers.append(CheckoutPaymentOptions.Provider.tabby) + } + if selectedPaymentMethodTypes.contains(.tamara) { + providers.append(CheckoutPaymentOptions.Provider.tamara) + } + return providers + #else + return [] + #endif + } + init() { - selectedPaymentMethodTypes = [.card, .applePay] + selectedPaymentMethodTypes = [.card, .applePay, .tabby, .tamara] } } extension MainViewModel { - func makeComponent() async { + func makeComponent() async throws { do { let paymentSession = try await createPaymentSession() paymentSessionId = paymentSession.id @@ -108,10 +135,13 @@ extension MainViewModel { checkoutComponentsView = renderedComponent } catch let error as CheckoutComponents.Error { errorMessage = error.localizedDescription - print(error.localizedDescription) + debugPrint(error.localizedDescription) + + throw error } catch { errorMessage = error.localizedDescription - print("Network error: \(error.localizedDescription).\nCheck if your keys are correct.") + debugPrint("Network error: \(error.localizedDescription).\nCheck if your keys are correct.") + throw error } } } @@ -119,24 +149,59 @@ extension MainViewModel { extension MainViewModel { // Step 1: Create Payment Session func createPaymentSession() async throws -> PaymentSession { - let request = PaymentSessionRequest(amount: 1, - currency: "GBP", - billing: .init(address: .init(country: "GB")), - customer: .init( - email: paymentSessionEmailModel, - name: "customerName", - phone: paymentSessionPhoneModel - ), - successURL: Constants.successURL, - failureURL: Constants.failureURL, - threeDS: .init(enabled: true, attemptN3D: true), - processingChannelID: selectedEnvironment == .sandbox ? EnvironmentVars.sandboxProcessingChannelID : nil, - paymentMethodConfiguration: .init(applepay: .init(totalType: selectedApplePayType.rawValue)), - locale: paymentSessionSelectedLocale.localeString) - - return try await networkLayer.createPaymentSession(request: request, - environment: selectedEnvironment) - } + let address = Address( + addressLine1: "11 New Burlington Street", + addressLine2: "Apt 214", + city: "London", + state: "London", + zip: "W1S 3BE", + country: selectedCountry.rawValue + ) + + let phone = Phone( + countryCode: "44", number: "08002580300" + ) + + let customer = Customer( + email: !paymentSessionUserEmail.isEmpty ? paymentSessionUserEmail : "customer+test@checkout.com", + name: !paymentSessionUsername.isEmpty ? paymentSessionUsername : "", + phone: paymentSessionPhoneModel + ) + + let paymentSessionRequest = PaymentSessionRequest( + amount: 10500, + currency: selectedCurrency.rawValue, + billing: BillingType(address: address, phone: phone), + reference: "cf72664f31984a7ab841d51b7305dc72", + description: "Set of 3 masks", + billingDescriptor: BillingDescriptor(name: "SUPERHEROES.COM", + city: "GOTHAM"), + shipping: Shipping(address: address, phone: phone), + metadata: Metadata(couponCode: "NY2018", + partnerId: 123989), + paymentType: nil, + successURL: Constants.successURL, + failureURL: Constants.failureURL, + threeDS: .init(enabled: true, attemptN3D: true), + processingChannelID: selectedEnvironment == .sandbox ? EnvironmentVars.sandboxProcessingChannelID : nil, + paymentMethodConfiguration: PaymentMethodConfiguration(applepay: ApplePayConfiguration(totalType: selectedApplePayType.rawValue)), + locale: paymentSessionSelectedLocale.localeString, + items: [ + Item(name: "Guitar", + quantity: 1, + unitPrice: 3500, + totalAmount: 3500), + Item(name: "Amp", + quantity: 2, + unitPrice: 3500, + totalAmount: 7000) + ], + customer: customer + ) + + return try await networkLayer.createPaymentSession(request: paymentSessionRequest, + environment: selectedEnvironment) + } // Step 2: Initialise an instance of Checkout Components SDK func initialiseCheckoutComponentsSDK(with paymentSession: PaymentSession) async throws (CheckoutComponents.Error) -> CheckoutComponents { @@ -147,7 +212,8 @@ extension MainViewModel { appearance: isDefaultAppearance ? .init() : DarkTheme().designToken, locale: selectedLocale.localeString, translations: getTranslation(), - callbacks: initialiseCallbacks()) + callbacks: initialiseCallbacks() + ) return CheckoutComponents(configuration: configuration) } @@ -156,11 +222,19 @@ extension MainViewModel { func createComponent(with checkoutComponentsSDK: CheckoutComponents) throws (CheckoutComponents.Error) -> Any { switch selectedComponentType { case .flow: - return try checkoutComponentsSDK.create(.flow(options: selectedPaymentMethods)) + return try checkoutComponentsSDK.create(.flow(options: selectedPaymentMethods, providers: apmProviders)) case .card: return try checkoutComponentsSDK.create(getCardPaymentMethod()) case .applePay: return try checkoutComponentsSDK.create(getApplePayPaymentMethod()) + #if canImport(CheckoutPaymentMethods) + case .tabby: + return try checkoutComponentsSDK.create(CheckoutPaymentOptions.Provider.tabby) + case .tamara: + return try checkoutComponentsSDK.create(CheckoutPaymentOptions.Provider.tamara) + #endif + default: + return try checkoutComponentsSDK.create(.flow(options: selectedPaymentMethods, providers: apmProviders)) } } @@ -202,18 +276,48 @@ extension MainViewModel { } } } - + + var isTabbySelected: Bool { + get { selectedPaymentMethodTypes.contains(.tabby) } + set { + if newValue { + selectedPaymentMethodTypes.insert(.tabby) + } else { + selectedPaymentMethodTypes.remove(.tabby) + } + } + } + + var isTamaraSelected: Bool { + get { selectedPaymentMethodTypes.contains(.tamara) } + set { + if newValue { + selectedPaymentMethodTypes.insert(.tamara) + } else { + selectedPaymentMethodTypes.remove(.tamara) + } + } + } + var selectedPaymentMethodsTitle: String { var selectedMethods: [String] = [] - + if isCardSelected { selectedMethods.append("Card") } - + if isApplePaySelected { selectedMethods.append("Apple Pay") } - + + if isTabbySelected { + selectedMethods.append("Tabby") + } + + if isTamaraSelected { + selectedMethods.append("Tamara") + } + if selectedMethods.isEmpty { return "Payment Methods" } else { @@ -224,15 +328,15 @@ extension MainViewModel { // Computed property to get actual payment methods with current configuration var selectedPaymentMethods: Set { var methods: Set = [] - + if selectedPaymentMethodTypes.contains(.card) { methods.insert(getCardPaymentMethod()) } - + if selectedPaymentMethodTypes.contains(.applePay) { methods.insert(getApplePayPaymentMethod()) } - + return methods } @@ -275,7 +379,7 @@ extension MainViewModel { func resetToDefaultConfiguration() { checkoutComponentsView = nil selectedComponentType = .flow - selectedPaymentMethodTypes = [.card, .applePay] + selectedPaymentMethodTypes = [.card, .applePay, .tabby, .tamara] showCardPayButton = true paymentButtonAction = .payment selectedLocale = .locale(.en_GB) diff --git a/SampleApplication/SampleApplication/NetworkLayer/NetworkLayer.swift b/SampleApplication/SampleApplication/NetworkLayer/NetworkLayer.swift index 4459148..ba519c6 100644 --- a/SampleApplication/SampleApplication/NetworkLayer/NetworkLayer.swift +++ b/SampleApplication/SampleApplication/NetworkLayer/NetworkLayer.swift @@ -17,14 +17,17 @@ struct NetworkLayer { // Don't send requests to this API but have a wrapper on your backend to keep your private key safe. // Otherwise you would have your private key bundled in your application and get it leaked. let baseURL = environment == .sandbox ? "https://api.sandbox.checkout.com" : "https://api.checkout.com" - let url = URL(string: "\(baseURL)/payment-sessions")! + guard let url = URL(string: "\(baseURL)/payment-sessions") else { + throw NSError(domain: "NetworkLayer", code: 0) + } var request = URLRequest(url: url) request.httpBody = requestBody request.httpMethod = "POST" let secretKey = environment == .sandbox ? EnvironmentVars.sandboxSecretKey : EnvironmentVars.productionSecretKey request.addValue("Bearer " + secretKey, forHTTPHeaderField: "Authorization") - + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + let (data, _) = try await URLSession.shared.data(for: request) let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase @@ -49,6 +52,7 @@ struct NetworkLayer { request.httpMethod = "POST" let secretKey = environment == .sandbox ? EnvironmentVars.sandboxSecretKey : EnvironmentVars.productionSecretKey request.addValue("Bearer " + secretKey, forHTTPHeaderField: "Authorization") + request.addValue("application/json", forHTTPHeaderField: "Content-Type") let (data, response) = try await URLSession.shared.data(for: request) if let response = response as? HTTPURLResponse { diff --git a/SampleApplication/SampleApplication/NetworkLayer/PaymentSessionRequest.swift b/SampleApplication/SampleApplication/NetworkLayer/PaymentSessionRequest.swift index a5d0224..e82189b 100644 --- a/SampleApplication/SampleApplication/NetworkLayer/PaymentSessionRequest.swift +++ b/SampleApplication/SampleApplication/NetworkLayer/PaymentSessionRequest.swift @@ -5,20 +5,32 @@ import Foundation struct PaymentSessionRequest: Encodable { let amount: Int let currency: String // Three letter ISO currency code - let billing: Billing - let customer: Customer? + let billing: BillingType + let reference: String + let description: String + let billingDescriptor: BillingDescriptor + let shipping: Shipping + let metadata: Metadata + let paymentType: String? let successURL: String let failureURL: String let threeDS: ThreeDS let processingChannelID: String? let paymentMethodConfiguration: PaymentMethodConfiguration let locale: String? - + let items: [Item] + let customer: Customer? + enum CodingKeys: String, CodingKey { + case amount, currency, reference, description, locale + case shipping, billing, metadata, items, customer + case billingDescriptor = "billing_descriptor" case threeDS = "3ds" + case successURL = "success_url" + case failureURL = "failure_url" + case paymentType = "payment_type" case processingChannelID = "processing_channel_id" case paymentMethodConfiguration = "payment_method_configuration" - case amount, currency, billing, customer, successURL, failureURL, locale } } @@ -30,35 +42,104 @@ struct BillingAddress: Encodable { let country: String } -struct Customer: Encodable { - let email: String? +struct ThreeDS: Encodable { + let enabled: Bool + let attemptN3D: Bool + enum CodingKeys: String, CodingKey { + case attemptN3D = "attempt_n3d" + case enabled + } +} + +struct PaymentMethodConfiguration: Encodable { + let applepay: ApplePayConfiguration +} + +struct ApplePayConfiguration: Encodable { + let totalType: String + + enum CodingKeys: String, CodingKey { + case totalType = "total_type" + } +} + +struct BillingDescriptor: Encodable { let name: String - let phone: Phone? + let city: String +} + +struct Address: Encodable { + let addressLine1: String + let addressLine2: String + let city: String + let state: String + let zip: String + let country: String + + enum CodingKeys: String, CodingKey { + case city, state, zip, country + case addressLine1 = "address_line1" + case addressLine2 = "address_line2" + } + + /// Fills non-country fields with empty strings so only `country` needs to be collected in the UI. + static func countryOnly(_ country: String) -> Address { + Address( + addressLine1: "Checkout.com", + addressLine2: "Checkout.com", + city: "Checkout.com", + state: "Checkout.com", + zip: "Checkout.com", + country: country + ) + } } struct Phone: Encodable { let countryCode: String? let number: String? - + enum CodingKeys: String, CodingKey { - case countryCode = "country_code" case number + case countryCode = "country_code" } } -struct ThreeDS: Encodable { - let enabled: Bool - let attemptN3D: Bool +struct Shipping: Encodable { + let address: Address + let phone: Phone +} + +struct BillingType: Encodable { + let address: Address + let phone: Phone? +} + +struct Metadata: Encodable { + let couponCode: String + let partnerId: Int + enum CodingKeys: String, CodingKey { - case attemptN3D = "attempt_n3d" - case enabled + case couponCode = "coupon_code" + case partnerId = "partner_id" } } -struct PaymentMethodConfiguration: Encodable { - let applepay: ApplePayConfiguration +struct Item: Encodable { + let name: String + let quantity: Int + let unitPrice: Int + let totalAmount: Int + + enum CodingKeys: String, CodingKey { + case name, quantity + case unitPrice = "unit_price" + case totalAmount = "total_amount" + } } -struct ApplePayConfiguration: Encodable { - let totalType: String +struct Customer: Encodable { + let email: String? + let name: String + let phone: Phone? }