# iOS

> Open Devzity checkout from an iOS app with ASWebAuthenticationSession and handle the return — without a secret key in your app.

Your app never talks to Devzity's payments API. It talks to **your small backend**, and that backend talks to Devzity:

```text
iOS app ──▶ your small backend ──(Devzity secret key)──▶ Devzity
   ▲                  │
   └──── checkoutUrl ─┘
iOS app ──▶ DevzityCheckout.present(checkoutURL) ──▶ customer pays ──▶ myapp://… ──▶ app
iOS app ──▶ your backend ──▶ "is my order paid?"       (the authoritative answer)
```

**The Devzity secret key stays on your backend.** This SDK never stores or uses it. Don't have a backend yet? Read [I only have an app](https://docs.devzity.com/v1/mobile-backend.md).

## 1. Add the package

`DevzityCheckout` is a Swift Package (iOS 15+, macOS 12+). It has one job: open the checkout page and read the return link.

**Swift SDK preview:** This package is not publicly published yet, so there is nothing to install from a registry.

- Add the sdk/ios directory as a local Swift package (`.package(path: "…/sdk/ios")`) while developing.

## 2. Present checkout

```swift
let result = try await DevzityCheckout.present(
    checkoutURL: checkoutURL,     // from YOUR backend, for an order
    callbackScheme: "myapp"       // your app's URL scheme
)
```

It uses `ASWebAuthenticationSession`, the system's secure browser sheet — never an in-app web view. The checkout URL must be `https`; `javascript:`, `data:`, `file:` and custom schemes are rejected with `DevzityCheckoutError.invalidCheckoutURL`. The `callbackScheme` is separate: the scheme your backend puts in the payment's `return_url` (for example `myapp://payment-return`).

`result` contains `paymentID`, `statusHint` and `returnURL`.

> **Warning:** `statusHint` is a **hint, never proof of payment.** Anyone can craft `myapp://payment-return?devzity_status=PAID`. Never fulfil or unlock anything because of it: refresh the order from **your backend**.

If the customer closes the sheet, `present` throws `DevzityCheckoutError.closedByUser`. That is **not** a failed payment — they may have already paid. Ask your backend for the order's status.

## SwiftUI

PayView.swift:

```swift
import SwiftUI
import DevzityCheckout

/// 1. Your backend creates the payment for an ORDER.  2. The app opens checkout.  3. The customer
/// returns through `myapp://payment-return`.  4. The app asks YOUR backend for the order's real status.
public struct PayView: View {
    private let backend = MerchantBackend(baseURL: URL(string: "https://your-backend.example.com")!)
    private let orderID = "order-1001"   // the order being paid; the server decides its amount
    @StateObject private var checkout = DevzityCheckoutSession()
    @State private var message = "Ready"

    public init() {}

    public var body: some View {
        VStack(spacing: 16) {
            Button("Pay for \(orderID)") {
                Task {
                    do {
                        let url = try await backend.checkoutURL(forOrder: orderID)
                        checkout.start(checkoutURL: url, callbackScheme: "myapp")
                    } catch {
                        message = "Could not start payment"
                    }
                }
            }
            Text(message)
        }
        .onChange(of: checkout.state) { state in
            switch state {
            case .returned:
                // result.statusHint is only a hint — never fulfil or unlock from it.
                // Refresh the order from YOUR backend.
                message = "Checking your payment…"
                Task { message = "Order status: \((try? await backend.status(ofOrder: orderID)) ?? "unknown")" }
            case .closed:
                // The customer closed the sheet. That is NOT a failed payment: ask your backend.
                message = "Checkout closed. Checking your order…"
                Task { message = "Order status: \((try? await backend.status(ofOrder: orderID)) ?? "unknown")" }
            case .failed:
                message = "Could not open checkout"
            case .idle, .presenting:
                break
            }
        }
    }
}
```

`DevzityCheckoutSession` is a tiny `ObservableObject` (states `idle`, `presenting`, `returned`, `closed`, `failed`). UIKit apps call `DevzityCheckout.present` from a `Task` and may pass their own `presentationAnchor`.

MerchantBackend.swift:

```swift
import Foundation

/// The app never talks to Devzity's payments API and never holds a secret key. It asks YOUR backend
/// about an ORDER: the backend decides the amount from its own order, creates the payment with its
/// secret key, and returns only the checkout_url. The app never sends an amount.
struct MerchantBackend {
    let baseURL: URL

    func checkoutURL(forOrder orderID: String) async throws -> String {
        var request = URLRequest(url: baseURL.appendingPathComponent("orders/\(orderID)/pay"))
        request.httpMethod = "POST"
        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode([String: String].self, from: data)["checkout_url"] ?? ""
    }

    /// The authoritative state of the order, from YOUR backend (which asks Devzity / receives its webhook).
    func status(ofOrder orderID: String) async throws -> String {
        let url = baseURL.appendingPathComponent("orders/\(orderID)/status")
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode([String: String].self, from: data)["status"] ?? "unknown"
    }
}
```

## Universal Links (recommended)

Custom URL schemes (`myapp://`) can be claimed by any other app. Where practical, use a **Universal Link** — an `https` link on a domain you own, verified with `apple-app-site-association` — as your `return_url` and call the iOS 17.4+ overload:

```swift
try await DevzityCheckout.present(
    checkoutURL: checkoutURL,
    universalLinkHost: "shop.example.com",
    path: "/payment-return"
)
```

Custom schemes remain fully supported and are the simplest way to start. If the app receives a link some other way (`onOpenURL`), `DevzityCheckout.parseReturn(url)` reads the same hints and returns `nil` for links that aren't Devzity returns.
