Link/iOS / Swift

iOS / Swift SDK

CoverPaySDK is a native Swift package that provides a Plaid-style drop-in BNPL checkout for iOS applications. It supports both SwiftUI and UIKit, presenting a secure modal checkout backed by a WKWebView bridge.

Requirements

RequirementMinimum Version
iOS16.0+
Swift5.9+
Xcode15.0+
macOS (for development)13.0+ (Ventura)

Installation

CoverPaySDK is distributed via Swift Package Manager (SPM). You can add it through Xcode or directly in your Package.swift.

Option 1: Xcode (Recommended)

1

In Xcode, go to File > Add Package Dependencies...

2

Enter the repository URL: https://github.com/jacksonhedge/coverpay-ios-sdk

3

Select "Up to Next Major Version" with version 1.0.0

4

Select your app target and click "Add Package"

5

Verify "CoverPaySDK" appears under your target's "Frameworks, Libraries, and Embedded Content"

Option 2: Package.swift

// Package.swift
dependencies: [
    .package(
        url: "https://github.com/jacksonhedge/coverpay-ios-sdk",
        from: "1.0.0"
    )
]

// Add to your target
targets: [
    .target(
        name: "YourApp",
        dependencies: [
            .product(name: "CoverPaySDK", package: "coverpay-ios-sdk")
        ]
    )
]

Option 3: Local Package (Development)

// For development with a local copy of the SDK
dependencies: [
    .package(path: "../CoverPay/sdk/ios")
]

Configuration

Initialize CoverPaySDK once when your app launches. This prepares the SDK and pre-loads the checkout web view for faster presentation.

SwiftUI (App init)

import SwiftUI
import CoverPaySDK

@main
struct MyApp: App {
    init() {
        CoverPayLink.setup(CoverPayConfiguration(
            clientId: "your_client_id",
            environment: .sandbox  // or .production
        ))
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

UIKit (AppDelegate)

import UIKit
import CoverPaySDK

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        CoverPayLink.setup(CoverPayConfiguration(
            clientId: "your_client_id",
            environment: .sandbox
        ))
        return true
    }
}

SwiftUI Integration

Use the .coverPayLink() view modifier to present the checkout as a native iOS sheet.

import SwiftUI
import CoverPaySDK

struct CheckoutView: View {
    @State private var showCoverPay = false
    @State private var paymentResult: String?

    let amount: Int = 9999 // cents

    var body: some View {
        VStack(spacing: 16) {
            Text("Total: $\(String(format: "%.2f", Double(amount) / 100))")
                .font(.title2)

            Button("Pay with BNPL") {
                showCoverPay = true
            }
            .buttonStyle(.borderedProminent)

            if let result = paymentResult {
                Text(result)
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
        }
        .coverPayLink(
            isPresented: $showCoverPay,
            params: CoverPayLinkParams(
                amount: amount,
                merchantName: "My Store",
                customerEmail: "user@example.com"
            ),
            onResult: { result in
                switch result {
                case .success(let payment):
                    paymentResult = "Paid via \(payment.provider)"
                    confirmPayment(token: payment.paymentToken)
                case .failure(let error):
                    paymentResult = "Error: \(error.localizedDescription)"
                }
            }
        )
    }

    private func confirmPayment(token: String) {
        // Send token to your server
        Task {
            var request = URLRequest(url: URL(string: "https://api.example.com/confirm")!)
            request.httpMethod = "POST"
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.httpBody = try? JSONEncoder().encode(["paymentToken": token])
            let (_, _) = try await URLSession.shared.data(for: request)
        }
    }
}

UIKit Integration

Use CoverPayLink.open() to present the checkout imperatively from any UIViewController.

import UIKit
import CoverPaySDK

class CheckoutViewController: UIViewController {

    @IBAction func payButtonTapped(_ sender: UIButton) {
        let params = CoverPayLinkParams(
            amount: 9999,
            merchantName: "My Store",
            customerEmail: "user@example.com"
        )

        CoverPayLink.open(
            params: params,
            from: self,
            onResult: { [weak self] result in
                switch result {
                case .success(let payment):
                    print("Token: \(payment.paymentToken)")
                    print("Provider: \(payment.provider)")
                    self?.confirmPayment(token: payment.paymentToken)

                case .failure(let error):
                    self?.showError(error)
                }
            }
        )
    }

    private func confirmPayment(token: String) {
        // Send token to your server
    }

    private func showError(_ error: CoverPayError) {
        let alert = UIAlertController(
            title: "Checkout Error",
            message: error.localizedDescription,
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }
}

Result Handling

The checkout returns a Result<CoverPayResult, CoverPayError>. Handle all error cases for a robust integration.

Success Result

public struct CoverPayResult {
    public let paymentToken: String
    public let provider: String      // "klarna", "affirm", etc.
    public let plan: CoverPayPlan
}

public struct CoverPayPlan {
    public let type: PlanType        // .payIn4, .monthly, .custom
    public let installments: Int
    public let installmentAmount: Int // cents
    public let totalAmount: Int      // cents
    public let apr: Double
}

Error Cases

public enum CoverPayError: Error, LocalizedError {
    /// User dismissed the checkout
    case cancelled

    /// User is not eligible for BNPL
    case notEligible(reason: String)

    /// The BNPL provider returned an error
    case providerError(provider: String, message: String)

    /// Network request failed
    case networkError(underlying: Error)

    /// SDK not configured or invalid parameters
    case configurationError(message: String)

    public var errorDescription: String? {
        switch self {
        case .cancelled:
            return "Checkout was cancelled"
        case .notEligible(let reason):
            return "Not eligible: \(reason)"
        case .providerError(let provider, let message):
            return "\(provider) error: \(message)"
        case .networkError(let error):
            return "Network error: \(error.localizedDescription)"
        case .configurationError(let message):
            return "Configuration error: \(message)"
        }
    }
}

Exhaustive handling example:

switch result {
case .success(let payment):
    confirmPayment(token: payment.paymentToken)

case .failure(.cancelled):
    // User tapped X or swiped down β€” no action needed
    break

case .failure(.notEligible(let reason)):
    showAlert("You're not eligible for BNPL: \(reason)")

case .failure(.providerError(let provider, let message)):
    showAlert("\(provider) could not process your payment: \(message)")
    analytics.track("bnpl_provider_error", [
        "provider": provider,
        "message": message,
    ])

case .failure(.networkError):
    showAlert("Please check your internet connection and try again.")

case .failure(.configurationError(let message)):
    // This should not happen in production
    assertionFailure("CoverPay config error: \(message)")
}

Architecture

CoverPaySDK presents a hosted checkout via WKWebView. Communication between the native layer and the web checkout uses a postMessage bridge, similar to Plaid Link.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ Your iOS App β”‚

β”‚ β”‚

β”‚ CoverPayLink.setup(config) β”‚

β”‚ .coverPayLink(isPresented:params:onResult:) β”‚

β”‚ β”‚

β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚

β”‚ β”‚ LinkViewController (UIKit modal) β”‚ β”‚

β”‚ β”‚ β”‚ β”‚

β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚

β”‚ β”‚ β”‚ WKWebView β”‚ β”‚ β”‚

β”‚ β”‚ β”‚ Loads: coverpayme.com/checkout β”‚ β”‚ β”‚

β”‚ β”‚ β”‚ β”‚ β”‚ β”‚

β”‚ β”‚ β”‚ JS ↔ Native via postMessage bridge β”‚ β”‚ β”‚

β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚

β”‚ β”‚ β”‚ β”‚

β”‚ β”‚ WebViewHandler: message routing + event decode β”‚ β”‚

β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚

β”‚ β”‚

β”‚ onResult: Result<CoverPayResult, CoverPayError> β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Bridge Events

EventDirectionPayloadDescription
readyWeb -> Native{}Checkout page loaded and ready
providerSelectedWeb -> Native{ provider, planType }User selected a BNPL provider
resizeWeb -> Native{ height }Content height changed (for dynamic sizing)
successWeb -> Native{ paymentToken, provider, plan }Payment authorized
errorWeb -> Native{ code, message }Error occurred
closeWeb -> Native{ reason }User closed the checkout
initNative -> Web{ clientId, amount, ... }Initialize checkout with params

SDK File Structure

CoverPaySDK/
β”œβ”€β”€ Package.swift
β”œβ”€β”€ README.md
β”œβ”€β”€ Sources/
β”‚   └── CoverPaySDK/
β”‚       β”œβ”€β”€ CoverPayLink.swift           # Main entry point + setup
β”‚       β”œβ”€β”€ CoverPayConfiguration.swift  # Config structs + params
β”‚       β”œβ”€β”€ CoverPayResult.swift         # Success result types
β”‚       β”œβ”€β”€ CoverPayError.swift          # Error enum
β”‚       └── Internal/
β”‚           β”œβ”€β”€ LinkViewController.swift # Modal presentation logic
β”‚           └── WebViewHandler.swift     # WKWebView + JS bridge
└── Examples/
    └── ExampleApp.swift                 # SwiftUI & UIKit examples

Troubleshooting

Common build and runtime issues when integrating CoverPaySDK.

Files created outside Xcode are NOT in the project target

If you create .swift files from the terminal, Finder, or an AI coding tool, they exist on disk but are not part of the Xcode build target. Xcode will not compile them, and references to types defined in those files will produce misleading errors. Always add new files to the target via File > Add Files to 'YourTarget'... or drag them into the Xcode navigator with the target checkbox enabled.

1. β€œTrailing closure passed to parameter of type 'any Decoder'”

Cause

A .swift file defining a type or view is on disk but not included in the Xcode target. The compiler cannot find the type and produces this misleading error instead of β€œcannot find type in scope.”

Fix

In Xcode, right-click your project group, select Add Files to 'YourTarget'..., select the missing .swift file, check the target checkbox, and click Add.

2. β€œCannot find type 'CoverPayLink' in scope”

Cause

The CoverPaySDK Swift package has not been added to your project, or the import CoverPaySDK statement is missing.

Fix

Add the package via File > Add Package Dependencies... and ensure import CoverPaySDK is at the top of your file.

3. β€œCannot find 'CoverPayError' in scope” (ambiguous)

Cause

Your project defines its own CoverPayError type that conflicts with CoverPaySDK.CoverPayError. The compiler cannot resolve which one you mean.

Fix

Qualify the SDK type with the module name: CoverPaySDK.CoverPayError. Or rename your local type (e.g., CoverPayServiceError).

4. β€œNo such module 'CoverPaySDK'”

Cause

The Swift package was added to the project but not linked to the correct build target.

Fix

Select your target in Xcode, go to General > Frameworks, Libraries, and Embedded Content, click the + button, and add CoverPaySDK.

5. @StateObject with CoverPayLink.shared singleton

Cause

Wrapping CoverPayLink.shared in @StateObject creates a new instance, conflicting with the singleton pattern. SwiftUI's @StateObject initializes the object itself, ignoring the existing .shared instance.

Fix

Access the singleton directly instead of wrapping it:

// Wrong
@StateObject private var link = CoverPayLink.shared

// Right β€” use the singleton directly
CoverPayLink.shared.open(params: ...) { result in
    // handle
}

6. Red filenames in Xcode navigator

Cause

A .swift file was deleted from disk (via terminal, Finder, or a tool) but the reference remains in the project.pbxproj. Xcode shows these as red filenames and may produce build errors about missing files.

Fix

In the Xcode navigator, select the red-colored file, press Delete, and choose Remove Reference. This removes the stale entry from project.pbxproj without touching other files.

CoverPayLinkParams Reference

PropertyTypeRequiredDescription
amountIntRequiredAmount in cents (e.g., 9999 = $99.99)
merchantNameStringRequiredYour business name
customerEmailString?OptionalPre-fill customer email
orderIdString?OptionalYour order ID for reconciliation
theme.light | .dark | .autoOptionalUI theme. Defaults to .auto.
preferredProviders[String]?OptionalPreferred provider order (e.g., ["klarna", "affirm"])

CoverPayConfiguration Reference

public struct CoverPayConfiguration {
    /// Your CoverPay API client ID
    public let clientId: String

    /// API environment
    public let environment: Environment

    /// Base URL override (for testing)
    public let baseURL: URL?

    public enum Environment {
        case sandbox
        case production
    }

    public init(
        clientId: String,
        environment: Environment,
        baseURL: URL? = nil
    )
}

Web SDKs available

For web applications, see the JavaScript SDK or React SDK. For server-side integration and webhooks, see the API Reference.