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
| Requirement | Minimum Version |
|---|---|
| iOS | 16.0+ |
| Swift | 5.9+ |
| Xcode | 15.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)
In Xcode, go to File > Add Package Dependencies...
Enter the repository URL: https://github.com/jacksonhedge/coverpay-ios-sdk
Select "Up to Next Major Version" with version 1.0.0
Select your app target and click "Add Package"
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
| Event | Direction | Payload | Description |
|---|---|---|---|
ready | Web -> Native | {} | Checkout page loaded and ready |
providerSelected | Web -> Native | { provider, planType } | User selected a BNPL provider |
resize | Web -> Native | { height } | Content height changed (for dynamic sizing) |
success | Web -> Native | { paymentToken, provider, plan } | Payment authorized |
error | Web -> Native | { code, message } | Error occurred |
close | Web -> Native | { reason } | User closed the checkout |
init | Native -> 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 examplesTroubleshooting
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'β
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.β
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β
The CoverPaySDK Swift package has not been added to your project, or the import CoverPaySDK statement is missing.
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)
Your project defines its own CoverPayError type that conflicts with CoverPaySDK.CoverPayError. The compiler cannot resolve which one you mean.
Qualify the SDK type with the module name: CoverPaySDK.CoverPayError. Or rename your local type (e.g., CoverPayServiceError).
4. βNo such module 'CoverPaySDK'β
The Swift package was added to the project but not linked to the correct build target.
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
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.
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
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.
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
| Property | Type | Required | Description |
|---|---|---|---|
amount | Int | Required | Amount in cents (e.g., 9999 = $99.99) |
merchantName | String | Required | Your business name |
customerEmail | String? | Optional | Pre-fill customer email |
orderId | String? | Optional | Your order ID for reconciliation |
theme | .light | .dark | .auto | Optional | UI theme. Defaults to .auto. |
preferredProviders | [String]? | Optional | Preferred 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.