Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions Adyen.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
0019C4C72E6B166800B0C3A3 /* UIProgressViewHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AD40E74262F04440090E01C /* UIProgressViewHelpers.swift */; };
0019C4D22E6ECE8700B0C3A3 /* BasicComponentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A026C85027C4FE4700E6C34A /* BasicComponentConfiguration.swift */; };
0019C4D42E6ECF5900B0C3A3 /* ViewIdentifierBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E441512382A0DD00D3CD48 /* ViewIdentifierBuilder.swift */; };
A1C0A0012F75000100ABCDEF /* CheckoutLocalizationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0A0022F75000100ABCDEF /* CheckoutLocalizationProvider.swift */; };
0019C4D52E6ED1E300B0C3A3 /* ViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7806EBC26147AB700101DBB /* ViewControllerDelegate.swift */; };
0019C4D62E6EEAA100B0C3A3 /* FormItemInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9589D022601EE7800E4113F /* FormItemInjector.swift */; };
0019C4D72E6EEAA100B0C3A3 /* AbstractPersonalInformationComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97C81AE25BAC8E600D7F85C /* AbstractPersonalInformationComponent.swift */; };
Expand Down Expand Up @@ -2158,6 +2159,7 @@
A01948332D8827A300AA27AC /* PayToFormPickerItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayToFormPickerItemView.swift; sourceTree = "<group>"; };
A01948542D91AD8700AA27AC /* CheckoutConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckoutConfiguration.swift; sourceTree = "<group>"; };
A01948562D92BFAB00AA27AC /* ConfigurationBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationBuilder.swift; sourceTree = "<group>"; };
A1C0A0022F75000100ABCDEF /* CheckoutLocalizationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckoutLocalizationProvider.swift; sourceTree = "<group>"; };
A01D775529B250C10075BD70 /* CashAppPayDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CashAppPayDetails.swift; sourceTree = "<group>"; };
A01DCF0A26BD67BB00BC35B3 /* FormCardExpiryDateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormCardExpiryDateItem.swift; sourceTree = "<group>"; };
A01DFBBE2BA887BF00205881 /* ThreadSafeAnalyticsEventDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSafeAnalyticsEventDataSource.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4975,6 +4977,7 @@
F99E7277261F6C6F00EA5B78 /* KeyboardObserver.swift */,
E216D3CE221AFC3A0013CBCF /* IBANSpecification.swift */,
E773EA35251E26BE00119499 /* Throttler.swift */,
A1C0A0022F75000100ABCDEF /* CheckoutLocalizationProvider.swift */,
);
path = Utilities;
sourceTree = "<group>";
Expand Down Expand Up @@ -8513,6 +8516,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A1C0A0012F75000100ABCDEF /* CheckoutLocalizationProvider.swift in Sources */,
E788A3B2265565DF00089448 /* EContextPaymentMethod.swift in Sources */,
E216D3C5221AEF450013CBCF /* IBANFormatter.swift in Sources */,
5A4702182664E9440023F264 /* APIContext.swift in Sources */,
Expand Down
137 changes: 137 additions & 0 deletions Adyen/Utilities/CheckoutLocalizationProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//
// Copyright (c) 2026 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import Foundation

// Key set mirrors the Android SDK `CheckoutLocalizationKey` enum for cross-platform alignment.
// Android: https://github.com/Adyen/adyen-android/blob/main/core/src/main/java/com/adyen/checkout/core/common/localization/CheckoutLocalizationKey.kt
/// A localization key passed to ``CheckoutLocalizationProvider``.
///
/// Compare against the well-known static members (e.g. `.cardNumber`) to identify which
/// string the SDK is requesting. New keys may be added in future SDK versions — always
/// include a `default` branch when switching over values.
public struct CheckoutLocalizationKey: Hashable {

/// The internal ``LocalizationKey`` used for SDK string resolution.
internal let localizationKey: LocalizationKey

internal init(localizationKey: LocalizationKey) {
self.localizationKey = localizationKey
}

/// Creates a key that has no direct ``LocalizationKey`` counterpart,
/// identified by its camelCase name within the `checkout.*` namespace.
internal init(name: String) {
localizationKey = LocalizationKey(key: "checkout.\(name)")
}

public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.localizationKey.key == rhs.localizationKey.key
}

public func hash(into hasher: inout Hasher) {
hasher.combine(localizationKey.key)
}
}

// swiftlint:disable identifier_name

// MARK: - General

extension CheckoutLocalizationKey {
public static let generalBack = CheckoutLocalizationKey(name: "generalBack")
public static let generalCancel = CheckoutLocalizationKey(localizationKey: .cancelButton)
public static let generalClose = CheckoutLocalizationKey(name: "generalClose")
public static let generalOptional = CheckoutLocalizationKey(localizationKey: .fieldTitleOptional)
public static let generalSearchHint = CheckoutLocalizationKey(localizationKey: .searchPlaceholder)
}

// MARK: - Card

extension CheckoutLocalizationKey {
public static let cardNumber = CheckoutLocalizationKey(localizationKey: .cardNumberItemTitle)
public static let cardNumberInvalid = CheckoutLocalizationKey(localizationKey: .cardNumberItemInvalid)
public static let cardNumberInvalidUnsupportedBrand = CheckoutLocalizationKey(localizationKey: .cardNumberItemUnsupportedBrand)
public static let cardExpiryDate = CheckoutLocalizationKey(localizationKey: .cardExpiryItemTitle)
public static let cardExpiryDateHint = CheckoutLocalizationKey(localizationKey: .cardExpiryItemPlaceholder)
public static let cardExpiryDateInvalid = CheckoutLocalizationKey(localizationKey: .cardExpiryItemInvalid)
public static let cardExpiryDateInvalidTooOld = CheckoutLocalizationKey(name: "cardExpiryDateInvalidTooOld")
public static let cardExpiryDateInvalidTooFarInTheFuture = CheckoutLocalizationKey(name: "cardExpiryDateInvalidTooFarInTheFuture")
public static let cardSecurityCode = CheckoutLocalizationKey(localizationKey: .cardCvcItemTitle)
public static let cardSecurityCodeHint3Digits = CheckoutLocalizationKey(name: "cardSecurityCodeHint3Digits")
public static let cardSecurityCodeHint4Digits = CheckoutLocalizationKey(name: "cardSecurityCodeHint4Digits")
public static let cardSecurityCodeInvalid = CheckoutLocalizationKey(localizationKey: .cardCvcItemInvalid)
public static let cardHolderName = CheckoutLocalizationKey(localizationKey: .cardNameItemTitle)
public static let cardHolderNameInvalid = CheckoutLocalizationKey(localizationKey: .cardNameItemInvalid)
public static let cardStorePaymentMethod = CheckoutLocalizationKey(localizationKey: .cardStoreDetailsButton)
public static let cardDualBrandSelectorTitle = CheckoutLocalizationKey(localizationKey: .creditCardDualBrandTitle)
public static let cardDualBrandSelectorDescription = CheckoutLocalizationKey(localizationKey: .creditCardDualBrandDescription)
public static let cardSocialSecurityNumber = CheckoutLocalizationKey(name: "cardSocialSecurityNumber")
public static let cardSocialSecurityNumberInvalid = CheckoutLocalizationKey(name: "cardSocialSecurityNumberInvalid")
}

// MARK: - Drop-in

extension CheckoutLocalizationKey {
public static let dropInManageFavoritesTitle = CheckoutLocalizationKey(name: "dropInManageFavoritesTitle")
public static let dropInManageFavoritesCardsSectionTitle = CheckoutLocalizationKey(name: "dropInManageFavoritesCardsSectionTitle")
public static let dropInManageFavoritesOthersSectionTitle = CheckoutLocalizationKey(name: "dropInManageFavoritesOthersSectionTitle")
public static let dropInManageFavoritesRemove = CheckoutLocalizationKey(name: "dropInManageFavoritesRemove")
public static let dropInManageFavoritesRemoveConfirmation = CheckoutLocalizationKey(name: "dropInManageFavoritesRemoveConfirmation")
public static let dropInOtherPaymentMethods = CheckoutLocalizationKey(name: "dropInOtherPaymentMethods")
public static let dropInPaymentMethodListDescription = CheckoutLocalizationKey(name: "dropInPaymentMethodListDescription")
// swiftlint:disable:next line_length
public static let dropInPaymentMethodListFavoritesSectionTitle = CheckoutLocalizationKey(name: "dropInPaymentMethodListFavoritesSectionTitle")
// swiftlint:disable:next line_length
public static let dropInPaymentMethodListFavoritesSectionAction = CheckoutLocalizationKey(name: "dropInPaymentMethodListFavoritesSectionAction")
// swiftlint:disable:next line_length
public static let dropInPaymentMethodListOptionsSectionTitle = CheckoutLocalizationKey(name: "dropInPaymentMethodListOptionsSectionTitle")
// swiftlint:disable:next line_length
public static let dropInPaymentMethodListOptionsTitleWithFavorites = CheckoutLocalizationKey(name: "dropInPaymentMethodListOptionsTitleWithFavorites")
public static let dropInPaymentMethodCardDescription = CheckoutLocalizationKey(name: "dropInPaymentMethodCardDescription")
}

// MARK: - Await

extension CheckoutLocalizationKey {
public static let awaitLoading = CheckoutLocalizationKey(localizationKey: .awaitWaitForConfirmation)
}

// MARK: - MBWay

extension CheckoutLocalizationKey {
public static let mbwayCountryCode = CheckoutLocalizationKey(name: "mbwayCountryCode")
public static let mbwayInvalidPhoneNumber = CheckoutLocalizationKey(name: "mbwayInvalidPhoneNumber")
public static let mbwayPhoneNumber = CheckoutLocalizationKey(name: "mbwayPhoneNumber")
}

// MARK: - BLIK

extension CheckoutLocalizationKey {
public static let blikCode = CheckoutLocalizationKey(localizationKey: .blikCode)
public static let blikCodeHint = CheckoutLocalizationKey(name: "blikCodeHint")
public static let blikCodeInvalid = CheckoutLocalizationKey(localizationKey: .blikInvalid)
public static let blikHelperText = CheckoutLocalizationKey(localizationKey: .blikHelp)
}

// swiftlint:enable identifier_name

/// An interface for providing selective programmatic overrides of Adyen Checkout UI strings.
///
/// Use this provider when you need to override a small number of strings.
/// Return a non-`nil` value only for the keys you want to override;
/// returning `nil` will let the SDK apply its normal localization fallback chain.
///
/// ## Adding support for a completely new language
/// This protocol is not the recommended path for adding a language that the SDK does not ship.
/// Instead, place a `.strings` or `.xcstrings` file in your app bundle with the `adyen.*` keys
/// translated into the target language. The SDK resolves strings from `Bundle.main` first,
/// so your translations are picked up automatically with no provider required.
///
// TODO: Provide reference doc/link to the file with all SDK keys
public protocol CheckoutLocalizationProvider {
func localizedString(_ key: CheckoutLocalizationKey, locale: Locale) -> String?
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public struct ThreeDS2ActionConfiguration: CheckoutComponentConfiguration {

package var localizationParameters: LocalizationParameters?

package var localizationProvider: (any CheckoutLocalizationProvider)?

package var redirectComponentStyle: RedirectComponentStyle?

/// `threeDSRequestorAppURL` for protocol version 2.2.0 OOB challenges.
Expand Down
2 changes: 2 additions & 0 deletions AdyenActions/Components/SDK/TwintActionConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public struct TwintActionConfiguration: CheckoutComponentConfiguration {
package var theme: AdyenTheme = .default

package var localizationParameters: LocalizationParameters?

package var localizationProvider: (any CheckoutLocalizationProvider)?

package var callbackAppScheme: String

Expand Down
1 change: 1 addition & 0 deletions AdyenCard/Components/BCMC/BCMCComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ private extension CardComponentConfiguration {
configuration.showsSupportedCardLogos = false
configuration.binLookupType = .bcmc
configuration.localizationParameters = localizationParameters
configuration.localizationProvider = localizationProvider
return configuration
}
}
12 changes: 8 additions & 4 deletions AdyenCard/Components/Card/CardComponentConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public struct CardComponentConfiguration: CheckoutComponentConfiguration, AnyPer

package var localizationParameters: LocalizationParameters?

// TODO: Phase 3 - wire this into CardComponent's string resolution to replace LocalizationParameters lookups.
package var localizationProvider: (any CheckoutLocalizationProvider)?

/// Indicates if the field for entering the holder name should be displayed in the form. Defaults to false.
internal var showsHolderNameField: Bool

Expand Down Expand Up @@ -71,7 +74,8 @@ public struct CardComponentConfiguration: CheckoutComponentConfiguration, AnyPer
/// Called when card brand(s) are detected from the entered card number.
internal var onBinLookup: (([CardBrand]) -> Void)?

// TODO: Add onFieldValidationChange closure that provides field validation updates including last 4 digits. or add it here after deciding on alignment
// TODO: Add onFieldValidationChange closure that provides field validation
// updates including last 4 digits, or add it here after deciding on alignment.

/// Initializes a new instance of `CardComponentConfiguration`.
public init() {
Expand Down Expand Up @@ -201,9 +205,9 @@ extension CardComponentConfiguration {
}

// TODO: find out if this field is needed. doesn't seem used
/// Sets the requirement policy for billing address.
/// - Parameter policy: The requirement policy (required, optional, or optional for specific card types).
/// - Returns: A modified copy of the configuration.
// /// Sets the requirement policy for billing address.
// /// - Parameter policy: The requirement policy (required, optional, or optional for specific card types).
// /// - Returns: A modified copy of the configuration.
// public func billingAddressRequirementPolicy(_ policy: BillingAddressConfiguration.RequirementPolicy) -> Self {
// var copy = self
// copy.billingAddress.requirementPolicy = policy
Expand Down
1 change: 1 addition & 0 deletions AdyenCheckout/CheckoutComponentBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ internal enum CheckoutComponentBuilder {

componentConfiguration.showsSubmitButton = configuration.showsSubmitButton
componentConfiguration.theme = configuration.theme
componentConfiguration.localizationProvider = configuration.localizationProvider

return factory.create(
with: paymentMethod,
Expand Down
22 changes: 21 additions & 1 deletion AdyenCheckout/CheckoutConfiguration/CheckoutConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public struct CheckoutConfiguration {

package var onComplete: CheckoutSuccessHandler?

package var localizationProvider: (any CheckoutLocalizationProvider)?

package var theme: AdyenTheme

package let amount: Amount?
Expand Down Expand Up @@ -101,13 +103,15 @@ public struct CheckoutConfiguration {
analyticsApiContext: APIContext?,
analyticsConfiguration: AnalyticsConfiguration,
configurations: [CheckoutComponentType: CheckoutComponentConfiguration] = [:],
localizationProvider: (any CheckoutLocalizationProvider)? = nil,
theme: AdyenTheme = .default
) {
self.analyticsConfiguration = analyticsConfiguration
self.analyticsApiContext = analyticsApiContext
self.amount = amount
self.apiContext = apiContext
self.configurations = configurations
self.localizationProvider = localizationProvider
self.theme = theme
}

Expand All @@ -129,7 +133,9 @@ public struct CheckoutConfiguration {
configurations[.action(actionType)] as? T
}

// TODO: Robert: Make public to private, This public is not needed. But currently using this to support providing analyticsAPIContext in the Integration Examples.
// TODO: Robert: Make public to private.
// This public is not needed, but it currently supports providing
// analyticsAPIContext in the Integration Examples.
package static func createAnalyticsAPIContext(
apiContext: APIContext
) -> APIContext? {
Expand Down Expand Up @@ -158,6 +164,20 @@ extension CheckoutConfiguration {
return copy
}

/// Sets a custom localization provider for programmatic string overrides.
///
/// The provider is called for each string the SDK renders. Return a non-`nil` value
/// to override the default, or return `nil` to let the SDK's standard localization
/// fallback chain handle the key (app bundle → SDK bundle → English).
///
/// - Note: To add support for a *completely new language*, place a `.strings` or
/// `.xcstrings` file with `adyen.*` keys in your app bundle instead of using this provider.
public func localizationProvider(_ localizationProvider: any CheckoutLocalizationProvider) -> Self {
Comment thread
atmamont marked this conversation as resolved.
var copy = self
copy.localizationProvider = localizationProvider
return copy
}

/// Sets the theme for the checkout configuration.
///
/// Use `AdyenTheme` builder methods to customize colors, attributes, and elements:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public struct ACHDirectDebitComponentConfiguration: AnyPersonalInformationConfig

package var localizationParameters: LocalizationParameters?

package var localizationProvider: (any CheckoutLocalizationProvider)?

package var showStorePaymentMethodField: Bool

package var showBillingAddress: Bool
Expand Down
2 changes: 2 additions & 0 deletions AdyenComponents/BLIK/BLIKComponentConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public struct BLIKComponentConfiguration: CheckoutComponentConfiguration {
package var theme: AdyenTheme = .default

package var localizationParameters: LocalizationParameters?

package var localizationProvider: (any CheckoutLocalizationProvider)?

public init(style: FormComponentStyle) {
self.init(style: style, localizationParameters: nil)
Expand Down
4 changes: 4 additions & 0 deletions AdyenUI/CheckoutConfiguration/CheckoutConfigurable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ package protocol CheckoutComponentConfiguration: CheckoutConfigurable {
// to be changed with new styling/localization

var localizationParameters: LocalizationParameters? { get }

// TODO: Consider non-optional with a no-op default once Phase 3 call sites are written.
var localizationProvider: (any CheckoutLocalizationProvider)? { get set }

// var style: FormComponentStyle { get }

var theme: AdyenTheme { get set }
Expand Down
Loading
Loading