Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/check-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ on:
- 'release/*'

env:
destination: "platform=iOS Simulator,name=iPhone 16 Pro,OS=latest"
destination: "platform=iOS Simulator,name=iPhone 17 Pro,OS=26.1"
configuration: "Debug"
noIndex: "COMPILER_INDEX_STORE_ENABLE=NO"
noSigning: "CODE_SIGNING_ALLOWED=NO"
versionXcode: "16.0"
versionXcode: "26.2"

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
Expand Down Expand Up @@ -126,7 +126,7 @@ jobs:

- name: Select Xcode
run: |
sudo xcode-select -switch /Applications/Xcode_16.0.app
sudo xcode-select -switch /Applications/Xcode_26.2.app

- name: Log xcodebuild Version
run: |
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,7 @@ contents.xcworkspacedata
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings

# End of https://www.toptal.com/developers/gitignore/api/macos,swift,xcode,carthage,cocoapods,swiftpm
# End of https://www.toptal.com/developers/gitignore/api/macos,swift,xcode,carthage,cocoapods,swiftpm

*.build
Package.resolved
4 changes: 2 additions & 2 deletions Checkout.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'Checkout'
s.version = '4.3.8'
s.version = '4.4.0'
s.summary = 'Checkout SDK for iOS'

s.description = <<-DESC
Expand All @@ -20,6 +20,6 @@ Pod::Spec.new do |s|
s.exclude_files = "Checkout/Samples/**"

s.dependency 'CheckoutEventLoggerKit', '~> 1.2.4'
s.dependency 'Risk', '~> 3.0.2'
s.dependency 'Risk', '~> 4.0.1'

end
40 changes: 36 additions & 4 deletions Checkout/Source/Model/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,52 @@
import Foundation

protocol BaseURLProviding {
var baseURL: URL { get }
var baseURL: URL? { get }
}

private enum APIHost {
static let production = "api.checkout.com"
Comment thread
This conversation was marked as resolved.
static let sandbox = "api.sandbox.checkout.com"

static func prefixed(_ prefix: String, environment: Environment) -> String {
switch environment {
case .production:
return "\(prefix).\(production)"
case .sandbox:
return "\(prefix).\(sandbox)"
}
}
}

/// Environment Enum for Production and Sandbox end points.
public enum Environment: String, BaseURLProviding {
case production
case sandbox

var baseURL: URL {
var baseURL: URL? {
let host: String
switch self {
case .production:
return URL(string: "https://api.checkout.com/")
host = APIHost.production
case .sandbox:
return URL(string: "https://api.sandbox.checkout.com/")
host = APIHost.sandbox
}
return URL(string: "https://\(host)/")
}
}

/// Provides a base URL for a given environment, optionally prefixed with a subdomain.
/// When `baseURLPrefix` is set (after trimming), URLs follow the pattern `{prefix}.api[.sandbox].checkout.com`.
struct EnvironmentURLProvider: BaseURLProviding {
let environment: Environment
let baseURLPrefix: String?

var baseURL: URL? {
let trimmed = baseURLPrefix?.trimmingCharacters(in: .whitespacesAndNewlines)
guard let prefix = trimmed, !prefix.isEmpty else {
return environment.baseURL
}
let host = APIHost.prefixed(prefix, environment: environment)
return URL(string: "https://\(host)/")
}
}
4 changes: 3 additions & 1 deletion Checkout/Source/Network/RequestFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ final class RequestFactory: RequestProviding {
}

func url(baseURLProvider: BaseURLProviding) -> Result<URL, RequestError> {
guard var urlComponents = URLComponents(url: baseURLProvider.baseURL, resolvingAgainstBaseURL: false) else {
guard let baseURL = baseURLProvider.baseURL,
var urlComponents = URLComponents(url:baseURL,
Comment thread
This conversation was marked as resolved.
resolvingAgainstBaseURL: false) else {
return .failure(.baseURLCouldNotBeConvertedToComponents)
}

Expand Down
7 changes: 4 additions & 3 deletions Checkout/Source/Tokenisation/CheckoutAPIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation
import UIKit
import CheckoutEventLoggerKit
import Risk
import RiskSDK

public protocol CheckoutAPIProtocol {
func createToken(_ paymentSource: PaymentSource, completion: @escaping (Result<TokenDetails, TokenisationError.TokenRequest>) -> Void)
Expand Down Expand Up @@ -36,7 +36,7 @@ final public class CheckoutAPIService: CheckoutAPIProtocol {

/// Initializes a CheckoutAPIService object with public key and Environment.
/// CheckoutAPIService holds the core tokenisation logic methods to tokenise a user’s card details
public convenience init(publicKey: String, environment: Environment) {
public convenience init(publicKey: String, environment: Environment, baseURLPrefix: String? = nil) {
let snakeCaseJSONEncoder = JSONEncoder()
let snakeCaseJSONDecoder = JSONDecoder()

Expand All @@ -46,7 +46,8 @@ final public class CheckoutAPIService: CheckoutAPIProtocol {
let cardValidator = CardValidator(environment: environment)

let networkManager = NetworkManager(decoder: snakeCaseJSONDecoder, session: .shared)
let requestFactory = RequestFactory(encoder: snakeCaseJSONEncoder, baseURLProvider: environment)
let baseURLProvider = EnvironmentURLProvider(environment: environment, baseURLPrefix: baseURLPrefix)
let requestFactory = RequestFactory(encoder: snakeCaseJSONEncoder, baseURLProvider: baseURLProvider)
let tokenRequestFactory = TokenRequestFactory(cardValidator: cardValidator, decoder: snakeCaseJSONDecoder)
let tokenDetailsFactory = TokenDetailsFactory()
let logManager = LogManager.self
Expand Down
64 changes: 64 additions & 0 deletions CheckoutTests/Model/EnvironmentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,67 @@ final class EnvironmentTests: XCTestCase {
XCTAssertEqual(subject.baseURL, URL(string: "https://api.sandbox.checkout.com/"))
}
}

final class EnvironmentURLProviderTests: XCTestCase {

// MARK: - No prefix (falls back to default environment URLs)

func test_baseURL_production_noPrefix() {
let subject = EnvironmentURLProvider(environment: .production, baseURLPrefix: nil)
XCTAssertEqual(subject.baseURL, URL(string: "https://api.checkout.com/"))
}

func test_baseURL_sandbox_noPrefix() {
let subject = EnvironmentURLProvider(environment: .sandbox, baseURLPrefix: nil)
XCTAssertEqual(subject.baseURL, URL(string: "https://api.sandbox.checkout.com/"))
}

func test_baseURL_production_emptyPrefix_fallsBackToDefault() {
let subject = EnvironmentURLProvider(environment: .production, baseURLPrefix: "")
XCTAssertEqual(subject.baseURL, URL(string: "https://api.checkout.com/"))
}

func test_baseURL_sandbox_emptyPrefix_fallsBackToDefault() {
let subject = EnvironmentURLProvider(environment: .sandbox, baseURLPrefix: "")
XCTAssertEqual(subject.baseURL, URL(string: "https://api.sandbox.checkout.com/"))
}

// MARK: - With prefix

func test_baseURL_production_withPrefix() {
let subject = EnvironmentURLProvider(environment: .production, baseURLPrefix: "msdd")
XCTAssertEqual(subject.baseURL, URL(string: "https://msdd.api.checkout.com/"))
}

func test_baseURL_sandbox_withPrefix() {
let subject = EnvironmentURLProvider(environment: .sandbox, baseURLPrefix: "msdd")
XCTAssertEqual(subject.baseURL, URL(string: "https://msdd.api.sandbox.checkout.com/"))
}

func test_baseURL_production_withDifferentPrefix() {
let subject = EnvironmentURLProvider(environment: .production, baseURLPrefix: "custom-subdomain")
XCTAssertEqual(subject.baseURL, URL(string: "https://custom-subdomain.api.checkout.com/"))
}

func test_baseURL_sandbox_withDifferentPrefix() {
let subject = EnvironmentURLProvider(environment: .sandbox, baseURLPrefix: "custom-subdomain")
XCTAssertEqual(subject.baseURL, URL(string: "https://custom-subdomain.api.sandbox.checkout.com/"))
}

// MARK: - Trimmed prefix

func test_baseURL_production_trimmedPrefix() {
let subject = EnvironmentURLProvider(environment: .production, baseURLPrefix: " msdd ")
XCTAssertEqual(subject.baseURL, URL(string: "https://msdd.api.checkout.com/"))
}

func test_baseURL_sandbox_trimmedPrefix() {
let subject = EnvironmentURLProvider(environment: .sandbox, baseURLPrefix: "\tmsdd\n")
XCTAssertEqual(subject.baseURL, URL(string: "https://msdd.api.sandbox.checkout.com/"))
}

func test_baseURL_production_whitespaceOnlyPrefix_fallsBackToDefault() {
let subject = EnvironmentURLProvider(environment: .production, baseURLPrefix: " ")
XCTAssertEqual(subject.baseURL, URL(string: "https://api.checkout.com/"))
}
}
4 changes: 2 additions & 2 deletions CheckoutTests/Stubs/StubBaseURLProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import Foundation

// swiftlint:disable force_unwrapping
final class StubBaseURLProvider: BaseURLProviding {
var baseURLToReturn = URL(string: "https://www.checkout.com/")!
var baseURLToReturn: URL? = URL(string: "https://www.checkout.com/")!
private(set) var baseURLCalled = false

var baseURL: URL {
var baseURL: URL? {
baseURLCalled = true
return baseURLToReturn
}
Expand Down
2 changes: 1 addition & 1 deletion CheckoutTests/Stubs/StubRisk.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//

import Foundation
@testable import Risk
@testable import RiskSDK
@testable import Checkout

// swiftlint:disable large_tuple
Expand Down
4 changes: 2 additions & 2 deletions Frames.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "Frames"
s.version = "4.3.8"
s.version = "4.4.0"
s.summary = "Checkout API Client, Payment Form UI and Utilities in Swift"
s.description = <<-DESC
Checkout API Client and Payment Form Utilities in Swift.
Expand All @@ -21,6 +21,6 @@ Pod::Spec.new do |s|

s.dependency 'PhoneNumberKit', '~> 4.0'
s.dependency 'CheckoutEventLoggerKit', '~> 1.2.4'
s.dependency 'Checkout', '4.3.8'
s.dependency 'Checkout', '4.4.0'

end
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ let package = Package(
from: "4.0.0"),
.package(
url: "https://github.com/checkout/checkout-risk-sdk-ios.git",
from: "3.0.2"),
from: "4.0.1"),
.package(
url: "https://github.com/checkout/checkout-event-logger-ios-framework.git",
from: "1.2.4"
Expand Down
6 changes: 3 additions & 3 deletions Source/Core/CheckoutAPIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import CheckoutEventLoggerKit
protocol CheckoutAPIProtocol {
var cardValidator: CardValidating { get }
var logger: FramesEventLogging { get }
init(publicKey: String, environment: Environment)
init(publicKey: String, environment: Environment, baseURLPrefix: String?)
func createToken(_ paymentSource: PaymentSource, completion: @escaping (Result<TokenDetails, TokenisationError.TokenRequest>) -> Void)
func createSecurityCodeToken(securityCode: String, completion: @escaping (Result<SecurityCodeTokenDetails, TokenisationError.SecurityCodeError>) -> Void)
}
Expand All @@ -23,8 +23,8 @@ public final class CheckoutAPIService: CheckoutAPIProtocol {
let logger: FramesEventLogging
private let checkoutAPIService: Checkout.CheckoutAPIProtocol

public init(publicKey: String, environment: Environment) {
let checkoutAPIService = Checkout.CheckoutAPIService(publicKey: publicKey, environment: environment.checkoutEnvironment)
public init(publicKey: String, environment: Environment, baseURLPrefix: String? = nil) {
let checkoutAPIService = Checkout.CheckoutAPIService(publicKey: publicKey, environment: environment.checkoutEnvironment, baseURLPrefix: baseURLPrefix)
let cardValidator = CardValidator(environment: environment.checkoutEnvironment)
let logger = FramesEventLogger(environment: environment, correlationID: checkoutAPIService.correlationID)

Expand Down
3 changes: 2 additions & 1 deletion Source/UI/PaymentForm/Factory/PaymentFormFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ public enum PaymentFormFactory {
let logger = FramesEventLogger(environment: configuration.environment, correlationID: UUID().uuidString)
let cardValidator = CardValidator(environment: configuration.environment.checkoutEnvironment)
let checkoutAPIService = CheckoutAPIService(publicKey: configuration.serviceAPIKey,
environment: configuration.environment)
environment: configuration.environment,
baseURLPrefix: configuration.baseURLPrefix)
var viewModel = DefaultPaymentViewModel(checkoutAPIService: checkoutAPIService,
cardValidator: cardValidator,
logger: logger,
Expand Down
6 changes: 5 additions & 1 deletion Source/UI/PaymentForm/Model/PaymentFormConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public struct PaymentFormConfiguration {
let environment: Environment
let supportedSchemes: [Card.Scheme]
let billingFormData: BillingForm?
let baseURLPrefix: String?

/**
Create a configuration for the Payment form
Expand All @@ -22,14 +23,17 @@ public struct PaymentFormConfiguration {
- environment: Enum describing the environment the SDK is running in
- supportedSchemes: Card schemes supported for receiving payments. Accurate declaration of supported schemes will improve customer experience
- billingFormData: Pre filled Billing form information to be included and help reduce user input if known
- baseURLPrefix: Optional subdomain prefix for the API base URL (e.g. "msdd" produces "msdd.api.checkout.com")
*/
public init(apiKey: String,
environment: Environment,
supportedSchemes: [CardScheme],
billingFormData: BillingForm?) {
billingFormData: BillingForm?,
baseURLPrefix: String? = nil) {
self.serviceAPIKey = apiKey
self.environment = environment
self.supportedSchemes = supportedSchemes.compactMap { $0.mapToCheckoutCardScheme() }
self.billingFormData = billingFormData
self.baseURLPrefix = baseURLPrefix
}
}
3 changes: 2 additions & 1 deletion Source/UI/SecurityCodeComponent/SecurityCodeComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ extension SecurityCodeComponent {

cardValidator = CardValidator(environment: configuration.environment.checkoutEnvironment)
checkoutAPIService = CheckoutAPIService(publicKey: configuration.apiKey,
environment: configuration.environment)
environment: configuration.environment,
baseURLPrefix: configuration.baseURLPrefix)

let viewModel = SecurityCodeViewModel(cardValidator: cardValidator)
if let initialCardScheme = configuration.cardScheme {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,27 @@ import UIKit
- If provided, card scheme's validation rules apply (e.g. VISA = 3 digits, American Express = 4 digits etc.)
- If not provided, security code is treated as valid for 3 and 4 digits
- style: Security Code Component wraps a text field in a secure way.
To style the inner properties like font, textColor etc, you must alter the style.
To style the inner properties like font, textColor etc, you must alter the style.
- baseURLPrefix: Optional subdomain prefix for the API base URL (e.g. "msdd" produces "msdd.api.checkout.com").
Whitespace is trimmed; empty or nil uses the default host.
*/

public struct SecurityCodeComponentConfiguration {
let apiKey: String
let environment: Environment
public var style: SecurityCodeComponentStyle
public var cardScheme: Card.Scheme?
let baseURLPrefix: String?

public init(apiKey: String,
environment: Environment,
style: SecurityCodeComponentStyle? = nil,
cardScheme: Card.Scheme? = nil) {
cardScheme: Card.Scheme? = nil,
baseURLPrefix: String? = nil) {
self.apiKey = apiKey
self.environment = environment
self.cardScheme = cardScheme
self.baseURLPrefix = baseURLPrefix

if let style = style {
self.style = style
Expand Down
2 changes: 1 addition & 1 deletion Tests/Mocks/StubCheckoutAPIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ final class StubCheckoutAPIService: Frames.CheckoutAPIProtocol {
private(set) var cardValidatorCalled = false
private(set) var loggerCalled = false

convenience init(publicKey: String, environment: Frames.Environment) {
convenience init(publicKey: String, environment: Frames.Environment, baseURLPrefix: String? = nil) {
self.init()
}

Expand Down
Loading
Loading