# Glomo iOS SDK v1

Official iOS SDK for integrating Glomo payment checkout flows into your native iOS applications. The public contract is deliberately aligned with the Flutter and Android SDKs.

> Current version: **v1.0.0**, the first stable release. The public API follows Semantic Versioning from this release onward.
Full [Changelog](/platform/sdk/ios-sdk/changelog) is also available.


## Prerequisites

Before using this SDK, you need:

- API credentials (Public Key) from your Glomo dashboard
- An order ID created via the Glomo API (or a subscription ID for subscription payments)


## System Requirements

| Requirement | Version |
|  --- | --- |
| iOS | 16.0 or later |
| Swift tools | 5.9 or later |
| Distribution | Swift Package Manager only |


CocoaPods is not supported.

## Installation

Add the package in Xcode (**File → Add Package Dependencies**) using the repository URL, and select the `v1.0.0` release tag:

```text
https://github.com/glomopay/glomo-ios-sdk.git
```

Or declare it in your own `Package.swift`:

```swift
dependencies: [
    .package(url: "https://github.com/glomopay/glomo-ios-sdk.git", from: "1.0.0"),
],
targets: [
    .target(
        name: "YourApp",
        dependencies: [.product(name: "glomo-ios-sdk", package: "glomo-ios-sdk")]
    ),
]
```

The package product is named `glomo-ios-sdk`. The Swift module is `GlomoPaySDK`:

```swift
import GlomoPaySDK
```

## Required Info.plist entries

Bank flows ask the user to upload documents. The checkout uses WebKit's own upload sheet, which presents Camera and Photo Library **from your application's process**, so your app must declare both usage descriptions or those options will not work:

```xml
<key>NSCameraUsageDescription</key>
<string>Camera access is used to capture documents during checkout verification.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Photo Library access is used to upload documents during checkout verification.</string>
```

Use wording that describes your own app. Without these keys, iOS terminates the app when the user picks those options.

## Quick Start

```swift
import GlomoPaySDK
import UIKit

final class CheckoutCoordinator: GlomoPayListener {

    private var handle: GlomoPayCheckoutHandle?

    func start(from presenter: UIViewController, orderId: String) {
        let config = GlomoPayConfig(
            publicKey: "live_your_public_key",
            orderId: orderId
        )
        handle = GlomoPaySDK.shared.startCheckout(
            from: presenter,
            config: config,
            listener: self
        )
    }

    func onPaymentSuccess(_ payload: GlomoPayPayload) {
        // Verify payload.signature on your server before fulfilling the order
    }

    func onPaymentFailure(_ payload: GlomoPayPayload) {
        // The backend confirmed the payment failed. No signature is carried here
    }

    func onUserJourneyCompleted(_ payload: GlomoPayUserJourneyPayload) {
        // Required. A journey ended without a confirmed payment - today only a
        // submitted bank transfer. No money has moved: reconcile server-side
    }

    func onSdkError(_ errors: [SdkError]) {
        // Validation, device compliance, or an order fault the backend answered
    }

    func onConnectionError(_ error: ConnectionError) {
        // Check error.shouldAutoClose before closing anything yourself
    }

    func onPaymentTerminate(_ source: TerminationSource) {
        // The checkout ended without a payment result
    }
}
```

**Retain the coordinator.** See [Listener retention](#listener-retention) - this is the single most common integration mistake on iOS.

## Listener retention

**The SDK holds your listener weakly.** It does not keep your object alive. Retain the listener for at least as long as the checkout runs.

A coordinator or handler that nothing else references is released mid-checkout, and the payment result then cannot be delivered. The SDK records that case in its own diagnostics rather than failing silently, but **the result is still gone** - there is no callback and no second chance.

```swift
// Wrong: nothing retains the coordinator, so the result is lost
GlomoPaySDK.shared.startCheckout(
    from: self,
    config: config,
    listener: CheckoutCoordinator()
)

// Right: the coordinator is held for the lifetime of the checkout
self.coordinator = CheckoutCoordinator()
self.handle = GlomoPaySDK.shared.startCheckout(
    from: self,
    config: config,
    listener: self.coordinator
)
```

Callbacks are delivered on the main queue. They must not throw or trap: Swift has no catchable exception mechanism, so a force-unwrap of `nil` or an out-of-range index inside a callback takes down the host process and the SDK cannot contain it.

## Subscriptions Checkout

To process subscription payments, pass a `subscriptionId` instead of an `orderId`:

```swift
let config = GlomoPayConfig(
    publicKey: "live_your_public_key",
    subscriptionId: "sub_your_subscription_id"
)
```

When `subscriptionId` is provided:

- The `subscriptionId` must start with `sub_`
- Do not pass both `orderId` and `subscriptionId` - the SDK will fire `onSdkError`


## Asynchronous Payment Journeys

Not every checkout ends in a payment the backend can confirm. When the user submits bank transfer details, no money has moved yet and there is nothing for your server to verify - no `paymentId` and no `signature`.

This ends the checkout through `onUserJourneyCompleted` with a `GlomoPayUserJourneyPayload`:

```swift
func onUserJourneyCompleted(_ payload: GlomoPayUserJourneyPayload) {
    switch payload.journeyType {
    case .bankTransfer:
        // Mark the order awaiting reconciliation. Do not fulfil it
        break
    }
}
```

It **does not also reach `onPaymentSuccess`**. One journey produces one callback.

`onUserJourneyCompleted` is required and has no protocol-extension default, unlike `onPaymentTerminate`. That is deliberate: the order decides server-side whether bank transfers are enabled, so a default body would let an integration compile and silently stop hearing about a journey it was told about before.

## Features

- **Standard, LRS, and Subscriptions** checkout flows
- **Automatic order type detection** from the order API, with explicit failure rather than a guess
- **Native page-sheet presentation** over your own view controller
- **WebView-based secure checkout** - payment data never passes through merchant code
- **Payment callbacks** with signature verification
- **Programmatic close** - `startCheckout` returns a handle that dismisses that session
- **Session isolation** - checkout cookies and storage never reach your app's own web views
- **Device security compliance** - jailbreak and debugger detection for live keys
- **Self-contained error reporting** - no Sentry package enters your dependency graph
- **Overridable strings** - every user-facing string can be replaced by your app
- **LRS education carousel** above the secure bank flow


## API Reference

### GlomoPaySDK

```swift
@MainActor
public func startCheckout(
    from presenter: UIViewController,
    config: GlomoPayConfig,
    orderType: String = "auto",
    listener: GlomoPayListener? = nil,
    animated: Bool = true,
    completion: (() -> Void)? = nil
) -> GlomoPayCheckoutHandle
```

Called on `GlomoPaySDK.shared`. Presents the checkout as a page sheet over `presenter` and returns a handle.

| Parameter | Type | Required | Description |
|  --- | --- | --- | --- |
| `presenter` | `UIViewController` | Yes | The view controller the checkout is presented from |
| `config` | `GlomoPayConfig` | Yes | Checkout configuration |
| `orderType` | `String` | No | `"auto"` (default, recommended), `"standard"`, or `"lrs"` |
| `listener` | `GlomoPayListener?` | No | Held **weakly** - see [Listener retention](#listener-retention) |
| `animated` | `Bool` | No | Whether the presentation is animated. Default `true` |
| `completion` | `(() -> Void)?` | No | Called once the presentation finishes |


`GlomoPaySDK.shared.validate(_:)` returns `[SdkError]` for a config without starting a checkout.

### GlomoPayCheckoutHandle

```swift
@MainActor
public final class GlomoPayCheckoutHandle {
    public func close()
}
```

Retain the handle to dismiss that checkout programmatically - for example when the host app signs the user out. The listener then receives `onPaymentTerminate(.programmatic)` exactly once.

`close()` called before the checkout has finished presenting is honoured once it appears. Called after the checkout has finished, it does nothing. The handle controls only the session it was returned for.

### GlomoPayConfig

```swift
public struct GlomoPayConfig: Sendable, Equatable {
    public init(
        publicKey: String,
        orderId: String? = nil,
        subscriptionId: String? = nil,
        autoCloseOnConnectionError: Bool = true
    )
}
```

| Property | Type | Default | Description |
|  --- | --- | --- | --- |
| `publicKey` | `String` | - | Your Glomo public key. Must start with `live_`, `test_`, or `mock_` |
| `orderId` | `String?` | `nil` | Order ID (starts with `order_`). Required unless using `subscriptionId` |
| `subscriptionId` | `String?` | `nil` | Subscription ID (starts with `sub_`). Mutually exclusive with `orderId` |
| `autoCloseOnConnectionError` | `Bool` | `true` | Whether a connectivity failure closes the checkout after reporting it |


Exactly one of `orderId` or `subscriptionId` must be provided. If both or neither are set, `onSdkError` fires.

`autoCloseOnConnectionError` applies only to failures the SDK marks as closing. An advisory render timeout, a cancelled navigation and an unmapped WebKit failure all show the retry surface instead, whatever this is set to.

`checkoutId` returns the active identifier and `isSubscription` reports whether a subscription ID was supplied.

### GlomoPayListener

```swift
public protocol GlomoPayListener: AnyObject {
    func onPaymentSuccess(_ payload: GlomoPayPayload)
    func onPaymentFailure(_ payload: GlomoPayPayload)
    func onSdkError(_ errors: [SdkError])
    func onConnectionError(_ error: ConnectionError)
    func onUserJourneyCompleted(_ payload: GlomoPayUserJourneyPayload)
    func onPaymentTerminate(_ source: TerminationSource)
}
```

| Callback | Required | Description |
|  --- | --- | --- |
| `onPaymentSuccess` | Yes | A payment the backend confirms succeeded. Verify `signature` server-side before fulfilling |
| `onPaymentFailure` | Yes | The backend confirms a payment failed. Carries no `signature` - a failure payload has never had one |
| `onSdkError` | Yes | The SDK or the order could not proceed - validation, forbidden device, or an order fault the backend answered |
| `onConnectionError` | Yes | Network, DNS, SSL, HTTP or WebView failure. Check `isRecoverable` and `shouldAutoClose` |
| `onUserJourneyCompleted` | Yes | A journey ended the checkout without a confirmed payment - today only `.bankTransfer` |
| `onPaymentTerminate` | No | The checkout ended without a payment result. Has a protocol-extension default |


**There is no `onEvent`.** The diagnostic channel it provided was never part of the integration contract; everything with diagnostic value goes to the SDK's own telemetry, which is sanitised and does not depend on your app implementing anything.

### GlomoPayPayload

Delivered by `onPaymentSuccess` and `onPaymentFailure`.

```swift
public struct GlomoPayPayload: Equatable {
    public let orderId: String
    public let paymentId: String?
    public let signature: String?
    public let rawResponse: [String: AnyHashable]?
}
```

**Merchant responsibility:** Always verify the `signature` on your server before fulfilling an order. Check it against each of your active secret keys, and accept it if any one matches. See [Checkout overview](/payin/checkout#steps-to-integrate-checkout) for verification instructions.

Do not expect a `signature` on `onPaymentFailure`. That field exists so a host can verify a success. The page's response travels verbatim in `rawResponse`.

### GlomoPayUserJourneyPayload

Delivered by `onUserJourneyCompleted`.

```swift
public struct GlomoPayUserJourneyPayload: Sendable, Equatable {
    public let journeyType: GlomoPayUserJourneyType
    public let orderId: String
    public let senderAccountNumber: String?
    public let transactionReference: String?
    public let status: String?
    public let rawResponse: [String: AnyHashable]?
}

public enum GlomoPayUserJourneyType: String, Sendable, CaseIterable {
    case bankTransfer
}
```

Deliberately not a `GlomoPayPayload`: `paymentId` and `signature` are the fields a host verifies a payment with, and neither exists for a journey. Reconcile it server-side against the order and never fulfil from it.

Journey fields are read in both camelCase and snake_case. A payload with no `orderId` is rejected rather than delivered.

The enum carries one member. Pay via Bank is not supported on the iOS SDK, so there is no journey member for it and the checkout page raises no event behind it - see [Pay via Bank](/payin/payment-methods/pay-via-bank) for that method's current availability.

### SdkError

```swift
public enum SdkErrorType: String, Sendable {
    case validationError
    case deviceForbidden
    case networkError
    case unknown
}

public struct SdkError: Error, Sendable, Equatable {
    public let type: SdkErrorType
    public let message: String
    public let field: String?
}
```

| Type | When |
|  --- | --- |
| `validationError` | Invalid public key, order ID, or subscription ID format. `field` indicates which failed |
| `deviceForbidden` | Jailbreak or debugger detected on a device using a `live_` key |
| `networkError` | The order could not be loaded before opening checkout |
| `unknown` | Unexpected error |


An order fault the backend actually answered - a non-2xx status, or a response that cannot be parsed - is reported here. A timeout or transport failure reports through `onConnectionError` instead.

### ConnectionError

```swift
public enum ConnectionErrorType: String, Sendable {
    case noInternet
    case dnsFailure
    case timeout
    case sslError
    case httpClientError
    case httpServerError
    case webResourceError
    case unknown
}

public struct ConnectionError: Error, Sendable, Equatable {
    public let type: ConnectionErrorType
    public let message: String
    public let statusCode: Int?
    public let failedURL: URL?
    public let errorCode: Int?
    public let shouldAutoClose: Bool
    public var isRecoverable: Bool { get }
}
```

| Type | Recoverable | Description |
|  --- | --- | --- |
| `noInternet` | Yes | Device is offline |
| `dnsFailure` | No | DNS resolution failed |
| `timeout` | Yes | Request timed out |
| `sslError` | No | SSL/TLS handshake failed |
| `httpClientError` | No | HTTP 4xx response |
| `httpServerError` | Yes | HTTP 5xx response |
| `webResourceError` | No | WebView resource loading failed |
| `unknown` | No | Unexpected error |


`shouldAutoClose` is decided from the mapped error type, not from the sign of `errorCode`. A cancelled navigation is not reported at all: WebKit delivers those routinely for superseded loads and client-side redirects, and treating one as a connection failure ended live checkouts for a non-event.

### TerminationSource

```swift
public enum TerminationSource: String, Sendable {
    case userDismiss
    case programmatic
    case connectionError
}
```

| Source | When |
|  --- | --- |
| `userDismiss` | The user closed the checkout from the navigation bar, or swiped the sheet down |
| `programmatic` | The host called `GlomoPayCheckoutHandle.close()` |
| `connectionError` | A connection error ended the checkout |


There is no `backButton` case. iOS has no native back control on the checkout; the navigation bar's Close button and interactive sheet dismissal both report `userDismiss`.

## Slow checkout warning

If the checkout page has not become interactive 15 seconds after presentation, the SDK reports `onConnectionError` with `ConnectionErrorType.timeout` and `shouldAutoClose` set to `false`, and shows a retry surface.

This is advisory, not an ending: the page may be seconds away and the session stays alive. If your `onConnectionError` dismisses the checkout for every error it receives, check `shouldAutoClose` first.

## Document Uploads

Bank flows that ask for documents open **WebKit's own upload sheet**, which offers Camera, Photo Library and Files on every supported iOS version. The SDK deliberately does not substitute its own document picker - doing so removed capture options the platform provides for free, on KYC fields where photographing a document is the common case.

Because WebKit presents Camera and Photo Library from your application's process, your app must declare `NSCameraUsageDescription` and `NSPhotoLibraryUsageDescription`. See [Required Info.plist entries](#required-infoplist-entries).

The SDK does not downscale captured images. Typical iPhone captures stay under the bank's upload limit.

## Checkout Behavior

- The checkout is presented as a page sheet over the view controller you pass in
- **Order type detection**: the flow is resolved from a successful order fetch, or the checkout does not open. A timeout or transport failure reports through `onConnectionError`, a non-2xx status or an unparseable response through `onSdkError`, and the session ends. No order type is guessed - guessing would route LRS traffic to the wrong checkout host
- **Bank flow overlay**: Back always closes the overlay and returns the user to the checkout, rather than walking the bank page's own history, which redirect chains make unexitable. Edge-swipe history gestures are disabled on both web views
- The bank overlay allows `http`, `https`, `about`, `blob` and `data` navigations and blocks everything else, so an unknown scheme cannot fail silently and strand the user on a page that appears to have done nothing
- **LRS education carousel**: for LRS orders an education strip is shown above the bank page when the checkout page reports content for it, and collapses to a fixed back bar when there is nothing to show


## Session Isolation

The checkout's web views share one non-persistent website data store. Checkout cookies, local storage and cache never reach your application's own `WKWebView` instances, and a second checkout never starts against what the first one left behind.

The SDK never clears app-wide website data - doing so would destroy a 3DS session mid-redirect.

## Device Security Compliance

| Scenario | Behavior |
|  --- | --- |
| `live_` key + jailbroken/debugged device | `onSdkError` with `deviceForbidden`. Checkout does not open |
| `live_` key + clean device | Checkout proceeds normally |
| `test_` or `mock_` key | Compliance checks skipped entirely |


## Mock Mode

| Key prefix | Mode | Description |
|  --- | --- | --- |
| `live_` | Production | Real payments. Device compliance enforced |
| `test_` | Test | Mock checkout. No real transactions. Compliance skipped |
| `mock_` | Mock | Same as `test_` |


## Input Validation

| Field | Rule |
|  --- | --- |
| `publicKey` | Length > 5. Must start with `live_`, `test_`, or `mock_` |
| `orderId` | Length > 6. Must start with `order_` |
| `subscriptionId` | Length > 4. Must start with `sub_` |


- Exactly one of `orderId` or `subscriptionId` must be provided
- Validation errors fire `onSdkError` with `validationError` and the `field` that failed
- `GlomoPaySDK.shared.validate(_:)` runs the same checks without starting a checkout


## Localization

Every user-facing string the SDK draws is looked up from a `GlomoPayLocalizable` table. Your app can override any key by declaring it in its own `GlomoPayLocalizable.strings`, because the main bundle is searched before the SDK's.

## Dependency Notes

**The SDK has no third-party package dependencies.** v1.0.0 removed the `sentry-cocoa` dependency: Swift Package Manager resolves one version of a package for the whole app, so a merchant already on a different Sentry version could not install the SDK at all. The SDK's own error reporting now runs over `URLSession`. You can use any Sentry version alongside it, or none.

No dSYM upload is required for the SDK. Its error reports carry no stack trace, and Glomo never receives your dSYMs.

## Troubleshooting

### No callbacks arrive at all

The listener was released. The SDK holds it weakly - see [Listener retention](#listener-retention). Retain the coordinator or handler for at least as long as the checkout.

### The app crashes when the user taps Camera or Photo Library

`NSCameraUsageDescription` or `NSPhotoLibraryUsageDescription` is missing from your app's `Info.plist`. See [Required Info.plist entries](#required-infoplist-entries).

### Package resolution fails on Sentry, or on a version conflict

v1.0.0 has no third-party dependencies. If resolution still fails, you are on an older build - move to `v1.0.0`.

### `onSdkError` fires immediately

Check that `publicKey` format is valid and that exactly one of `orderId` / `subscriptionId` is provided. `GlomoPaySDK.shared.validate(_:)` reports the same errors without opening a checkout.

### `deviceForbidden` in development

Use a `test_` or `mock_` key prefix during development.

### `onConnectionError` with `timeout` but the checkout is still open

This is the 15-second [slow checkout warning](#slow-checkout-warning), reported with `shouldAutoClose` false. The session is still alive.

### Bank transfers do not reach `onPaymentSuccess`

Expected. They arrive at `onUserJourneyCompleted`, because they carry no `paymentId` and no `signature` for your backend to verify. See [Asynchronous Payment Journeys](#asynchronous-payment-journeys).

### The build fails on a missing `onUserJourneyCompleted`

It is required and has no default implementation. If you do not take bank transfers, an empty body is a complete integration - it simply never fires.

## Security

- Payment data is handled within a secure, isolated WebView - it never passes through your application code
- The JavaScript bridge is limited to payment lifecycle events; no PAN, CVV, or sensitive payment data crosses it
- Jailbreak and debugger detection blocks checkout on compromised devices (live keys only)
- Every URL the page asks to open is validated before it can become a navigation. A parse is not a check, so `javascript:`, `data:` and `file:` URLs are rejected rather than followed
- The bank flow overlay allows only `http`, `https`, `about`, `blob` and `data` navigations
- Checkout web views use a non-persistent data store isolated from your app's own web views
- The SDK installs no crash or exception handlers, swizzles nothing, and never touches a Sentry client your app owns
- The bundled privacy manifest declares the SDK's data use; tracking is disabled and no App Tracking Transparency prompt is required


To report a security vulnerability, email security@glomopay.com. Do not open a public GitHub issue for security reports.

## Related

- [Changelog](/platform/sdk/ios-sdk/changelog) - all release notes
- [Android SDK](/platform/sdk/android-sdk) - Glomo SDK for native Android apps
- [React Native SDK](/platform/sdk/react-native-sdk) - Glomo SDK for React Native apps
- [Flutter SDK](/platform/sdk/flutter-sdk) - Glomo SDK for Flutter apps
- [Unified SDK (Web)](/platform/sdk/unified-sdk) - Glomo SDK for web apps
- [Checkout overview](/payin/checkout) - server-side checkout integration and signature verification