Migrating to iOS 3DS SDK v3.0

This guide covers breaking and behavioural changes when upgrading from v2.x to v3.0.

Minimum deployment target remains iOS 12.0. Swift concurrency helpers require iOS 13.0+.


Summary of breaking changes

Areav2.xv3.0
SDK constructionThreeDS2SDK(configParameters:…) convenience initThreeDS2SDK() then initialize(…)
Init completion((Bool) -> Void)?(Result<Void, R3DS2Error>) -> Void
Init locale parameterPresent (ignored by SDK)Removed
UI customization at initSingle UiCustomization?UiCustomizationMap? (DEFAULT / DARK / MONOCHROME)
Dark-mode colourssetDarkTextColor, setDarkBackgroundColor, etc.Removed — use separate map entries
SDK-owned UI stringsHardcoded EnglishLocalized via SDK string catalog (English shipped)
Init failure when metadata unavailableCould complete with success == falseResult.failure(.sdkRuntime(…)) with explicit message

Non-breaking additions: initialize(…) async throws and createTransaction(…) async throws on iOS 13+.


1. Initialization

v2.x

let sdk = ThreeDS2SDK(configParameters: config, uiCustomization: customization) { result in
    switch result {
    case .success: 
    case .failure: 
    }
}

// or legacy Bool callback
try sdk.initialize(configParameters: config, locale: nil, uiCustomization: customization) { success in
    if success {  }
}

v3.0 — canonical pattern

Always construct first, then initialize:

let sdk = ThreeDS2SDK()

do {
    try sdk.initialize(configParameters: config, uiCustomization: customizationMap) { result in
        switch result {
        case .success:
            // SDK ready — safe to call getSDKVersion(), createTransaction(), etc.
            break
        case .failure(let error):
            // Async failure — typically metadata/operability (see §1.5)
            break
        }
    }
} catch {
    // Synchronous failure — invalid config or SDKAlreadyInitialized
}

v3.0 — Swift concurrency (iOS 13+)

let sdk = ThreeDS2SDK()

do {
    try await sdk.initialize(configParameters: config, uiCustomization: customizationMap)
} catch let error as R3DS2Error {
    
} catch {
    
}

v3.0 — single DEFAULT customization (convenience)

If you only need one theme and do not use dark/monochrome maps, the protocol still provides a convenience overload:

try sdk.initialize(configParameters: config, uiCustomization: singleCustomization) { result in
    
}

This wraps the customization as [.DEFAULT: singleCustomization]. The async throws overload accepts UiCustomizationMap? only.

Removed APIs

These no longer exist in v3.0:

  • init(configParameters:uiCustomization:completion:)
  • initialize(…, completion: ((Bool) -> Void)?)
  • initialize(…, locale:…) (all overloads)

1.1 locale parameter

The locale argument has been removed from initialization. It was never used by the SDK.

Device-information .locale (C005) is derived from Locale.current; .systemLocale (I010) from NSLocale.system, with a fallback to en_US_POSIX when the root system locale exposes no language-region components. Neither uses the removed init locale parameter.

Action: Remove locale: from all initialize call sites.

1.2 Result-based completion

Replace Bool callbacks with Result<Void, R3DS2Error>:

// v2.x
try sdk.initialize(configParameters: config, uiCustomization: map) { success in
    guard success else { return }
    
}

// v3.0
try sdk.initialize(configParameters: config, uiCustomization: map) { result in
    switch result {
    case .success:
        
    case .failure(let error):
        
    }
}

1.3 Synchronous vs asynchronous errors

WhenHow it surfaces
Invalid config (e.g. missing PAK)Thrown synchronously from initialize(…) on the caller’s thread
Second init while already initialized / in progressThrown synchronously — .sdkAlreadyInitialized
Metadata download or operability check failsDelivered in completion / async throw — .sdkRuntime(message:)

1.4 Threading

After synchronous validation passes, metadata download and operability checks run on a background queue (DispatchQueue.global(qos: .userInitiated)).

Result-based completion — the handler is not called on the main queue:

try sdk.initialize(configParameters: config, uiCustomization: map) { result in
    DispatchQueue.main.async {
        switch result {
        case .success:    // update UI here
        case .failure: 
        }
    }
}

async throws (iOS 13+) — the awaiting task resumes on the main actor after metadata work completes. Synchronous validation failures from initialize(…) still throw on the caller’s thread before background work starts.

try await sdk.initialize(configParameters: config, uiCustomization: map)
// safe to update UI here when called from a MainActor context

Both async methods are not cancellable; once started, they run to completion.

Each ThreeDS2SDK instance should be used serially — do not call initialize, createTransaction, or cleanup concurrently on the same instance from multiple threads or unstructured Tasks.

1.5 Initialization operability

v3.0 reports metadata problems explicitly instead of silently completing with false:

FailureTypical R3DS2ErrorMessage
SDK version metadata unavailable.sdkRuntimeunable to obtain SDK version metadata
Required scheme RIDs missing from cache.sdkRuntimeunable to obtain scheme data for: <RID>, …

Configure required RIDs via the RIDS config parameter (ConfigParamType.registeredApplicationProviderIdentifiers). When omitted, init requires only usable SDK version metadata.


2. UI customization

2.1 UiCustomizationMap

Pass a map keyed by UI mode instead of a single customization object:

let defaultTheme = UiCustomization()
// configure defaultTheme …

let darkTheme = UiCustomization()
// configure darkTheme …

let map: UiCustomizationMap = [
    .DEFAULT: defaultTheme,
    .DARK: darkTheme,
    .MONOCHROME: darkTheme   // optional; falls back to DEFAULT
]

try sdk.initialize(configParameters: config, uiCustomization: map) { result in  }

The SDK resolves the active entry at challenge UI time based on the current trait collection (light / dark / grayscale accessibility), and refreshes when traits change.

2.2 Removed dual-colour APIs

v2.x allowed light and dark colours on the same customization object:

// v2.x — removed in v3.0
try toolbar.setTextColor(hexColorCode: "#000000")
try toolbar.setDarkTextColor(hexColorCode: "#FFFFFF")
try toolbar.setBackgroundColor(hexColorCode: "#FFFFFF")
try toolbar.setDarkBackgroundColor(hexColorCode: "#1E293B")

v3.0 uses one colour set per UiCustomization instance. Provide separate instances in the map:

// v3.0
try defaultToolbar.setTextColor(hexColorCode: "#1E1955")
try defaultToolbar.setBackgroundColor(hexColorCode: "#5C46F6")

try darkToolbar.setTextColor(hexColorCode: "#F1F5F9")
try darkToolbar.setBackgroundColor(hexColorCode: "#1E293B")

let map: UiCustomizationMap = [
    .DEFAULT: defaultUi,
    .DARK: darkUi
]

Removed methods (all customization types):

  • setDarkTextColor(hexColorCode:)
  • setDarkBackgroundColor(hexColorCode:)
  • setDarkHeadingTextColor(hexColorCode:)
  • setDarkBorderColor(hexColorCode:)
  • getDarkTextColor(), getDarkBackgroundColor(), etc.

2.3 Renamed getter

v2.xv3.0
getTextboxCustomization()getTextBoxCustomization()

3. Localization

SDK-owned challenge chrome (toolbar defaults, cancel confirmation, whitelist Yes/No, alert buttons, OOB error dialog) is now loaded from the SDK’s string catalog (Localizable.xcstrings, English included in the XCFramework).

String sourceWhat it controls
SDK string catalogDefault toolbar title/cancel, cancel dialog, whitelist buttons, generic alerts
UiCustomization toolbar textOverrides catalog defaults when set
ACS challenge contentUnchanged — provided by the ACS

Action: If you previously relied on hardcoded English matching SDK defaults, behaviour is unchanged for English. For other languages, either:

  • Override text via ToolbarCustomization.setHeaderText / setButtonText, or
  • Wait for additional catalog locales in a future SDK release.

Security warning IDs (SW01SW05) are unchanged and remain developer-facing English strings.


4. Unchanged public APIs

The following remain compatible (signatures unchanged):

  • createTransaction(directoryServerID:messageVersion:) — sync throw and Result-based completion; see §4.1 for the new async overload
  • getAuthenticationRequestParameters(), doChallenge(…), getProgressView(), close()
  • ChallengeStatusReceiver callbacks
  • ConfigParameters / addParam(…) / ConfigParamType
  • R3DS2Error cases: .invalidInput, .sdkAlreadyInitialized, .sdkNotInitialized, .sdkRuntime
  • Typealiases: ThreeDS2SDK, ConfigParameters, UiCustomization, etc.

4.1 Swift concurrency (iOS 13+) — new

try await sdk.initialize(configParameters: config, uiCustomization: map)

let transaction = try await sdk.createTransaction(
    directoryServerID: "A000000004",
    messageVersion: "2.2.0"
)

let result = try await transaction.doChallenge(
    challengeParameters: challengeParams,
    timeOut: 5,
    challengeView: challengeView
)

initialize, createTransaction, and doChallenge async throws methods wrap the existing callback-based APIs and resume on the main actor. They do not support cancellation. Use the completion-based overloads when you need background-queue delivery or iOS 12 support.

doChallenge returns a R3DS2ChallengeResult for terminal challenge outcomes (completed, cancelled, timedOut, protocolError, runtimeError). Synchronous validation failures are still thrown as R3DS2Error. The callback-based ChallengeStatusReceiver API remains available and unchanged.


5. EMVCo lab builds (TESTING_BUILD)

Lab / certification builds expose a synchronous init without network metadata:

try sdk.initialize(configParameters: config, uiCustomization: map)

Changes from v2.x lab builds:

  • locale parameter removed
  • uiCustomization is UiCustomizationMap?, not UiCustomization?

Production Release XCFramework builds do not include TESTING_BUILD APIs.


6. Migration checklist

Code changes

  • Replace ThreeDS2SDK(configParameters:…) with ThreeDS2SDK() + initialize(…)
  • Replace Bool init completions with Result<Void, R3DS2Error>
  • Remove all locale: arguments
  • Convert single UiCustomization? to UiCustomizationMap with .DEFAULT (and .DARK / .MONOCHROME as needed)
  • Replace setDark* calls with separate customization instances per mode
  • Rename getTextboxCustomization()getTextBoxCustomization() if used
  • Handle .sdkRuntime init failures for metadata/operability
  • Dispatch completion-based init/transaction callbacks to the main queue before UI updates (async throws resumes on the main actor)
  • Use one SDK instance serially per checkout flow

Configuration and OOB

  • If using Universal App Links for OOB return on protocol 2.3.1, add oobUniversalAppLinkSupported = "true" at init
  • Continue providing requestor app URL via ChallengeParameters.setThreeDSRequestorAppURL(...) for OOB return

Backend and telemetry

  • Coordinate with your backend team if you consume Ravelin SDK error reports — v3.0 uses SdkEventCode and EventDiagnostics (aligned with Android)
  • Validate telemetry ingestion for new event codes and consolidated lifecycle events
OptionConfigurationWhen to use
A — Dynamic (default)Embed & sign v3.0.0 as for v2.0.1. No MERGED_BINARY_TYPE.Simplest v2 → v3 upgrade
B — MergeableSet MERGED_BINARY_TYPE on app target (Xcode 15+).Apps using mergeable libraries
  • Update to v3.0.0 XCFramework (SPM, CocoaPods ~> 3.0.0, or manual embed)
  • Option A: Embed & sign; no merge build settings (default)
  • Option B: Set MERGED_BINARY_TYPE on host app target
  • Confirm CI Release builds match your chosen link model
  • Re-test challenge UI in light, dark, and grayscale accessibility modes
  • Test all challenge types on a physical device (text, select, OOB, web)
  • Test OOB: leave to external app, return to foreground — challenge should complete without error
  • Verify timeout and cancel callbacks fire correctly
  • Call getWarnings() on a clean production Release build on a physical device
  • iOS 12 smoke test if still supported (completion APIs only)

Reference integration: see SDKManager.swift in the demo workspace (ravelin-3ds-sdk-ios-workspace/DemoUIKitLocalRef) for v3 init patterns with both completion handlers and async throws.


7. Complete v3.0 example

func setupSDK() {
    let config = ConfigParameters()
    try? config.addParam(paramType: .publishableApiKey, paramValue: "<your-pak>")

    let defaultUi = buildDefaultUiCustomization()
    let darkUi = buildDarkUiCustomization()
    let map: UiCustomizationMap = [.DEFAULT: defaultUi, .DARK: darkUi]

    let sdk = ThreeDS2SDK()

    if #available(iOS 13.0, *) {
        Task { @MainActor in
            do {
                try await sdk.initialize(configParameters: config, uiCustomization: map)
                onSDKReady(sdk)
            } catch {
                onSDKFailed(error)
            }
        }
    } else {
        do {
            try sdk.initialize(configParameters: config, uiCustomization: map) { result in
                DispatchQueue.main.async {
                    switch result {
                    case .success: onSDKReady(sdk)
                    case .failure(let error): onSDKFailed(error)
                    }
                }
            }
        } catch {
            onSDKFailed(error)
        }
    }
}

Feedback