> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dodopayments.com/llms.txt
> Use this file to discover all available pages before exploring further.

# iOS

> 从 iOS 应用中的 SFSafariViewController 打开 Dodo Payments 托管结账，使用官方 Swift SDK，并通过一次调用获取类型化的结账结果。

> [!NOTE]
>
> 本页面介绍官方 Dodo Payments iOS Swift 结账 SDK。它会在原生浏览器视图中打开 Dodo Payments 托管结账，并返回类型化结果。

> **[Checkout Sessions API](/developer-resources/checkout-session)**
>
> 从后端创建此 SDK 打开的 `checkout_url`。

> **[Mobile Integration Guide](/developer-resources/mobile-integration)**
>
> 了解此 SDK 如何融入完整的移动支付流程。

iOS SDK 会在 `SFSafariViewController` 中打开 Dodo Payments 托管结账，并在客户完成或离开结账时返回类型化的 `CheckoutResult`。它不保存 API key，也不包含网络代码，因此不会调用 Dodo Payments API。结账在浏览器视图中运行。SDK 负责呈现和关闭该视图，并从返回 URL 中读取结果。

\*\*要求：\*\*iOS 16 或更高版本，以及 Swift 6.2 或更高版本（该包声明 `swift-tools-version: 6.2`）。SDK 没有第三方依赖。

## 安装

**1. Add the Package**

在 Xcode 中，前往 **File → Add Package Dependencies**，然后输入包 URL：

```text theme={null}
https://github.com/dodopayments/dodopayments-mobile-sdk-ios
```

选择 1.1.0 或更高版本。[外观自定义](#appearance-customization)要求使用 1.1.0。

若要改为在 `Package.swift` 中添加该包，请添加以下依赖：

```swift Package.swift theme={null}
.package(url: "https://github.com/dodopayments/dodopayments-mobile-sdk-ios", from: "1.1.0")
```

库产品为 `DodoCheckout`。

**2. Register a Callback URL Scheme**

注册 URL scheme，使 iOS 将结账的返回 URL 路由回你的应用。在你的 `Info.plist` 中添加 URL 类型：

```xml Info.plist theme={null}
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>myapp</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
    </array>
  </dict>
</array>
```

你也可以在 Xcode 的 **Info → URL Types** 下添加 URL 类型。

在传递给 SDK 的 `returnUrl` 中使用此 scheme，例如 `myapp://checkout/return`；同时，在后端创建会话时，将同一个 URL 设置为结账会话的 `return_url`。SDK 会根据 scheme、host 和 path 匹配返回 URL。该 URL 不需要加载真实页面。

## 使用

`DodoCheckout.start` 是一个在主 actor 上运行的 `async` 函数。将后端返回的 `checkout_url` 构建为 `URL`，并将其作为 `checkoutUrl` 传入：

```swift theme={null}
import DodoCheckout

// showSuccess(_:), showFailure(), showExpired(), and reconcileAbandonedSession()
// are your own functions. See Abandoned Sessions for what to reconcile.
let result = try await DodoCheckout.start(
    checkoutUrl: checkoutUrl,   // from your backend's checkout session
    returnUrl: URL(string: "myapp://checkout/return")!,
    onEvent: { event in print(event.name) }  // logging only
)

switch result.status {
case .succeeded: showSuccess(result.paymentId)
case .failed:    showFailure()
case .cancelled: await reconcileAbandonedSession()  // outcome unknown, not a failure
case .pending:   await reconcileAbandonedSession()
case .expired:   showExpired()
}
```

`onEvent` 接收 `.opened`、`.returnReceived` 和 `.closed` 事件。它们的 `name` 值分别为 `checkout.opened`、`checkout.return_received` 和 `checkout.closed`。事件仅用于日志记录，切勿用其决定结果。

## 转发返回 URL

`SFSafariViewController` 无法捕获自己的返回 URL，因此 iOS 会改为在你的应用中打开该 URL。将每个传入的 URL 转发给 `DodoCheckout.handleOpenURL(_:)`。在不使用 scenes 的应用中，请从应用 delegate 的 `application(_:open:options:)` 中调用它。

**SwiftUI**

```swift theme={null}
.onOpenURL { url in
    DodoCheckout.handleOpenURL(url)
}
```

**SceneDelegate**

```swift SceneDelegate.swift theme={null}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    guard let url = URLContexts.first?.url else { return }
    DodoCheckout.handleOpenURL(url)
}
```

> [!NOTE]
>
> 你可以转发每个 URL。`handleOpenURL` 只会处理与当前结账的 `returnUrl` 匹配的 URL，并为其返回 `true`。对于其他 URL，它会返回 `false`，因此请自行处理该 URL。

## 结果含义

SDK 会根据返回 URL 中的查询参数构建 `CheckoutResult`。

> [!WARNING]
>
> `result.status` 只是 UI 提示，并不能证明支付成功。请通过后端使用 INLINE\_CODE\_PLACEHOLDER\_bfeed7a4ab5393d\_END 或 `subscription.active` webhook 确认每笔支付。

**status** `CheckoutStatus` *required*

五个值之一：

* `succeeded`：返回 URL 包含 `status=succeeded`（一次性支付）或 `status=active`（订阅）。
* `failed`：支付被拒绝（`status=failed`）。
* `cancelled`：返回 URL 到达前，客户关闭了面板。SDK 不知道结果，并且支付可能已经成功，因此不要显示失败页面。请改为对[已放弃的会话](#abandoned-sessions)进行对账。
* `pending`：支付稍后结算（`status=processing` 或任何 `requires_*` 值），或者 `status` 参数缺失或无法识别。请按照 `cancelled` 的方式进行对账。
* `expired`：结账会话已过期（`status=expired`）。

**paymentId** `String?`

返回 URL 包含 `payment_id` 查询参数时，该参数的值。请在 UI 中显示它，但不要用它授予访问权限。请参阅[验证支付](#verify-the-payment)。

**subscriptionId** `String?`

`subscription_id` 查询参数。用于订阅结账。

**licenseKeys** `[String]?`

`license_key` 查询参数。结账包含 license key 产品时设置。

**customerEmail** `String?`

`email` 查询参数。结账捕获电子邮件地址时设置。

**raw** `[String: String]`

返回 URL 中的每个查询参数，原样保留。

## 验证支付

> **[Webhooks](/developer-resources/webhooks)**
>
> 支付成功或订阅激活时，Dodo Payments 会调用你的后端。

> **[Get Payment Detail](/api-reference/payments/get-payments-1)**
>
> 使用你的 secret key 查询 `paymentId`，以检查其状态。

仅在上述方式之一确认支付后授予访问权限。不要仅依赖 `result.status`。

## 外观自定义

要更改面板的关闭按钮、呈现样式和配色方案，请将一个 `BrowserCustomization` 作为 `customization` 传递给 `start(...)`。每个字段都是可选的。对于 `nil` 字段，SDK 不会设置该选项，而是由 iOS 应用自身的默认值。例外情况是 `presentationStyle`，此时 `nil` 表示 `pageSheet`。

**dismissButtonStyle** `DismissButtonStyle?`

关闭按钮的样式：`done`、`close` 或 `cancel`。iOS 会决定将其渲染为标签还是图标。

**presentationStyle** `PresentationStyle?`

`pageSheet`（默认值）会呈现一个客户可以向下滑动关闭的卡片。`fullScreen` 会覆盖整个屏幕，且没有关闭手势。

**barCollapsingEnabled** `Bool?`

允许工具栏在页面滚动时折叠。只有当 `presentationStyle` 为 `fullScreen` 时，该设置才会产生可见效果。使用 `pageSheet` 时，无论此设置如何，栏都会固定不动。

**colorScheme** `ColorScheme?`

`light` 或 `dark` 会强制使用对应外观，无论设备的系统设置如何。`system` 会遵循系统设置。此选项只会设置页面周围的原生控件主题。结账页面自身的浅色或深色模式来自结账会话中的 `customization.theme`，其颜色来自 `customization.theme_config`。

iOS 没有工具栏颜色选项。底层 `SFSafariViewController` 的 tint 属性自 iOS 26 起已弃用。

```swift theme={null}
let result = try await DodoCheckout.start(
    checkoutUrl: checkoutUrl,
    returnUrl: URL(string: "myapp://checkout/return")!,
    customization: BrowserCustomization(
        dismissButtonStyle: .close,
        presentationStyle: .fullScreen,
        colorScheme: .dark
    )
)
```

## 错误

`start` 仅会因误用或平台故障抛出 `CheckoutError`。请从 `error.code` 中读取原因。客户取消结账或支付被拒绝时，始终会返回结果，而不是抛出错误。

* `invalidCheckoutUrl`（`INVALID_CHECKOUT_URL`）：`checkoutUrl` 不是 INLINE\_CODE\_PLACEHOLDER\_4f449422f8a8b80ca\_END 结账会话 URL（路径以 `/session/` 开头），且不在 `checkout.dodopayments.com` 或 `test.checkout.dodopayments.com` 上。
* `invalidReturnUrl`（`INVALID_RETURN_URL`）：`returnUrl` 不是包含 scheme 和 host 的绝对 URL。
* `alreadyInProgress`（`ALREADY_IN_PROGRESS`）：另一个结账正在运行。一次只能运行一个结账。
* `platformError`（`PLATFORM_ERROR`）：意外的平台故障，例如没有可用于呈现视图的 view controller。

抛出错误后，也要检查是否存在已放弃的会话。如果面板未确认已显示，SDK 会保留该会话记录，因为结账可能仍处于打开状态。例外是 `alreadyInProgress`：此时找到的记录属于仍在运行的结账。

## 已放弃的会话

> [!NOTE]
>
> SDK 会在呈现结账时记录结账会话，并且仅当结账以 `succeeded`、`failed` 或 `expired` 结束时清除该记录。如果应用在结账期间被终止，或者返回 `cancelled` 或 `pending` 结果，记录会保留。请在下次启动时，以及每次返回 `cancelled` 或 `pending` 结果后检查该记录。

```swift theme={null}
import DodoCheckout

if let abandoned = DodoCheckout.getAbandonedSession() {
    // Ask your backend for the outcome of abandoned.sessionId, and show it.
    // Clear the record only after the outcome is final:
    DodoCheckout.clearAbandonedSession()
}
```

`abandoned.sessionId` 是结账会话 ID，以 `cks_` 开头。`abandoned.createdAt` 是启动结账的 `Date`。你的后端可以使用 [获取结账会话](/api-reference/checkout-sessions/get-checkouts) 查询该会话，该接口会返回其 `payment_id` 和 `payment_status`。在支付达到最终状态之前，请将其视为待处理，而不是失败。

## 相关内容

> **[Mobile Integration Guide](/developer-resources/mobile-integration)**
>
> Android、React Native 和 Flutter 使用相同的契约。

> **[React Native SDK](/developer-resources/sdks/react-native)**
>
> 在 iOS 上封装相同的 Swift 核心。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.