> ## 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.

# Android

> 从 Android 应用中通过 Custom Tab 和官方 Android SDK 打开 Dodo Payments 托管结账，并通过一次调用获取类型化的结账结果。

> [!NOTE]
>
> 本页面介绍 Android 结账 SDK（`com.dodopayments.api:checkout-android`），该 SDK 会在你的应用内打开 Dodo Payments 托管结账页面。若要从服务器调用 Dodo Payments API，请改用 [backend Kotlin SDK](/developer-resources/sdks/kotlin)。

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

> **[Mobile Integration Guide](/developer-resources/mobile-integration)**
>
> 移动端结账流程的最佳实践。

Android SDK 会在 Custom Tab（`androidx.browser.customtabs`）中打开 Dodo Payments 托管结账页面，并在客户完成或离开结账时返回一个类型化的 `CheckoutResult`。你的后端创建结账会话，并将其 `checkout_url` 发送到应用。SDK 不包含网络代码，也不持有 API 密钥，因此不会调用 Dodo Payments API。

**要求：** `minSdk` 23、Kotlin 和 Java 17。SDK 仅依赖 `androidx.activity`、`androidx.browser` 和 `kotlinx-coroutines-android`。

## 安装

**1. Add the Dependency**

从 Maven Central 将 SDK 添加到应用模块的 `build.gradle.kts` 中：

```kotlin build.gradle.kts theme={null}
dependencies {
    implementation("com.dodopayments.api:checkout-android:1.1.0")
}
```

[外观自定义](#appearance-customization)需要 1.1.0 或更高版本。

**2. Register a Callback URL Scheme**

将回调 scheme 设置为 Gradle manifest placeholder。SDK 自身的 manifest 会使用 `${dodoCallbackScheme}` placeholder 声明重定向 activity 的 intent filter，因此这是唯一需要执行的设置步骤。你无需添加任何 manifest XML：

```kotlin build.gradle.kts theme={null}
android {
    defaultConfig {
        manifestPlaceholders["dodoCallbackScheme"] = "myapp"
    }
}
```

在 `CheckoutParams.returnUrl` 中使用相同的 scheme，例如 `myapp://checkout/return`，并在后端创建会话时，将相同的 URL 设置为结账会话的 `return_url`。SDK 会根据 scheme、host 和 path 匹配返回 URL，并忽略 query string。该 URL 不需要加载真实页面。

> [!NOTE]
>
> 如果省略该 placeholder，构建会因 unresolved-placeholder 错误而失败。如果 placeholder 与 `returnUrl` 的 scheme 不匹配，SDK 会在打开结账前抛出 `PLATFORM_ERROR`。

## 使用

SDK 提供两种启动结账的方式：activity result launcher 和 suspend function。两者都会返回相同的 `CheckoutResult`。

**Launcher (Recommended)**

使用 `registerForActivityResult` 注册 contract，然后启动它：

```kotlin theme={null}
import com.dodopayments.checkout.CheckoutParams
import com.dodopayments.checkout.CheckoutStatus
import com.dodopayments.checkout.DodoCheckout

// showSuccess(), showFailure(), showExpired(), and reconcileAbandonedSession()
// are your own functions. See Abandoned Sessions for what to reconcile.
private val checkoutLauncher =
    registerForActivityResult(DodoCheckout.contract()) { result ->
        when (result.status) {
            CheckoutStatus.SUCCEEDED -> showSuccess(result.paymentId)
            CheckoutStatus.FAILED -> showFailure()
            CheckoutStatus.CANCELLED -> reconcileAbandonedSession() // outcome unknown, not a failure
            CheckoutStatus.PENDING -> reconcileAbandonedSession()
            CheckoutStatus.EXPIRED -> showExpired()
        }
    }

checkoutLauncher.launch(
    CheckoutParams(
        checkoutUrl = checkoutUrl, // from your backend's checkout session
        returnUrl = "myapp://checkout/return"
    )
)
```

> [!TIP]
>
> 在可能的情况下，请使用此方式。Android 会通过其 `ActivityResultRegistry` 传递结果，因此即使进程终止，结果也能保留。

**Suspend Function**

从 coroutine scope 中调用 `DodoCheckout.start`：

```kotlin theme={null}
import androidx.lifecycle.lifecycleScope
import com.dodopayments.checkout.CheckoutParams
import com.dodopayments.checkout.CheckoutStatus
import com.dodopayments.checkout.DodoCheckout
import kotlinx.coroutines.launch

// showSuccess(), showFailure(), showExpired(), and reconcileAbandonedSession()
// are your own functions. See Abandoned Sessions for what to reconcile.
lifecycleScope.launch {
    val result = DodoCheckout.start(
        activity = this@MyActivity,
        params = CheckoutParams(
            checkoutUrl = checkoutUrl, // from your backend's checkout session
            returnUrl = "myapp://checkout/return"
        ),
        onEvent = { event -> println(event.name) } // logging only
    )

    when (result.status) {
        CheckoutStatus.SUCCEEDED -> showSuccess(result.paymentId)
        CheckoutStatus.FAILED -> showFailure()
        CheckoutStatus.CANCELLED -> reconcileAbandonedSession() // outcome unknown, not a failure
        CheckoutStatus.PENDING -> reconcileAbandonedSession()
        CheckoutStatus.EXPIRED -> showExpired()
    }
}
```

`onEvent` 会接收 `checkout.opened`、`checkout.return_received` 和 `checkout.closed` 事件。仅将它们用于日志记录，不要用它们来决定结果。

> [!WARNING]
>
> 此方式会等待内存中的 `CompletableDeferred`，因此结果**无法**在进程终止后保留。`onEvent` 仅在此方式中可用，使用 launcher 时不可用。

## 结果含义

SDK 会根据返回 URL 中的 query parameters 构建 `CheckoutResult`。

> [!WARNING]
>
> `status` 字段只是 UI 提示，并不能证明付款已完成。在授予访问权限之前，请通过 webhook 或 Get Payment Detail endpoint 在后端确认付款。

**status** `CheckoutStatus` *required*

五个值之一：

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

**paymentId** `String?`

返回 URL 包含 `payment_id` query parameter 时，该参数会存在。你可以在 UI 中显示它，但不要用它来授予访问权限。请参阅[验证付款](#verify-the-payment)。

**subscriptionId** `String?`

`subscription_id` query parameter。用于订阅结账。

**licenseKeys** `List\<String\>?`

`license_key` query parameter。在结账包含 license key 产品时设置。

**customerEmail** `String?`

`email` query parameter。在结账收集电子邮件地址时设置。

**raw** `Map\<String, String\>`

返回 URL 中的每个 query parameter，原样保留。

## 验证付款

> **[Webhooks](/developer-resources/webhooks)**
>
> 实时监听付款事件。

> **[Get Payment Detail](/api-reference/payments/get-payments-1)**
>
> 按需查询付款状态。

只有在以下任一方式确认付款后，才授予访问权限，例如通过 `payment.succeeded` 或 `subscription.active` webhook。不要仅依赖 `CheckoutResult.status`。

## 外观自定义

若要更改 Custom Tab 的工具栏、按钮和配色方案，请在 `CheckoutParams` 上将 `BrowserCustomization` 作为 `customization` 传入。每个字段都是可选的，默认为 `null`。对于值为 `null` 的字段，SDK 不会设置该选项，因此承载 Custom Tab 的浏览器会应用其自身的默认值。

**toolbarColor** `Int?`

工具栏背景颜色，使用 ARGB `Color` int 表示。

**navigationBarColor** `Int?`

导航栏颜色，使用 ARGB `Color` int 表示。

**navigationBarDividerColor** `Int?`

导航栏上方分隔线的颜色，使用 ARGB `Color` int 表示。

**closeButtonStyle** `CloseButtonStyle?`

`DEFAULT` 显示系统的“X”图标。`BACK` 显示由 SDK 绘制的返回箭头。

**closeButtonPosition** `CloseButtonPosition?`

关闭按钮出现的工具栏一侧：`START` 或 `END`。

**shareButtonEnabled** `Boolean?`

显示工具栏的分享图标。`false` 会将其隐藏。

**showTitleEnabled** `Boolean?`

在工具栏的 URL 下方显示页面标题。

**urlBarHidingEnabled** `Boolean?`

页面滚动时自动隐藏工具栏。

**bookmarksButtonEnabled** `Boolean?`

在溢出菜单中显示“将此页面加入书签”。

**downloadsButtonEnabled** `Boolean?`

在溢出菜单中显示“下载页面”。

**colorScheme** `ColorScheme?`

`LIGHT` 或 `DARK` 会强制使用相应外观，而不考虑设备的系统设置。`SYSTEM` 遵循系统设置。

此示例复用了[用法](#usage)中的 `checkoutLauncher`：

```kotlin theme={null}
import android.graphics.Color
import com.dodopayments.checkout.BrowserCustomization
import com.dodopayments.checkout.CheckoutParams

checkoutLauncher.launch(
    CheckoutParams(
        checkoutUrl = checkoutUrl,
        returnUrl = "myapp://checkout/return",
        customization = BrowserCustomization(
            toolbarColor = Color.parseColor("#6366F1"),
            closeButtonStyle = BrowserCustomization.CloseButtonStyle.BACK,
            colorScheme = BrowserCustomization.ColorScheme.DARK,
        )
    )
)
```

## 错误

`DodoCheckout.start` 仅在误用或平台故障时抛出 `CheckoutError`。请从 `CheckoutError.code` 中读取原因：

* `INVALID_CHECKOUT_URL`：`checkoutUrl` 不是 `https` 结账会话 URL（路径以 `/session/` 开头），且不位于 `checkout.dodopayments.com` 或 `test.checkout.dodopayments.com` 上。
* `INVALID_RETURN_URL`：`returnUrl` 不是包含 scheme 和 host 的绝对 URL。
* `ALREADY_IN_PROGRESS`：另一个结账正在运行。一次只能运行一个结账流程。
* `PLATFORM_ERROR`：意外的平台故障，包括 `returnUrl` scheme 与 `dodoCallbackScheme` placeholder 不匹配的情况。

客户取消结账或付款被拒绝时，始终会返回结果（`CANCELLED` 或 `FAILED`），而不会抛出错误。使用 launcher 时，验证错误会从 `launcher.launch(...)` 抛出。启动后发生的平台故障无法通过 activity result callback 抛出，因此 launcher 会返回 `CANCELLED`，并将错误代码放入 `raw["error"]`。

## 已放弃的会话

SDK 会在结账开始时记录结账会话，并且仅当结账以 `SUCCEEDED`、`FAILED` 或 `EXPIRED` 结束时清除记录。如果应用在结账过程中被终止，记录会保留；在返回 `CANCELLED` 或 `PENDING` 结果后，记录也会保留，因为在这些情况下 SDK 不知道结果。请在下一次应用启动时，以及每次获得 `CANCELLED` 或 `PENDING` 结果后检查该记录：

```kotlin theme={null}
// context is any Context, such as your Activity.
DodoCheckout.getAbandonedSession(context)?.let { abandoned ->
    // Ask your backend for the outcome of abandoned.sessionId, and show it.
    // Clear the record only after the outcome is final:
    DodoCheckout.clearAbandonedSession(context)
}
```

`abandoned.sessionId` 是结账会话 ID，以 `cks_` 开头。`abandoned.createdAt` 是结账开始时间，以毫秒为单位的 epoch timestamp。你的后端可以使用 [Get Checkout Session](/api-reference/checkout-sessions/get-checkouts) 查询会话，该接口会返回其 `payment_id` 和 `payment_status`。在付款达到最终状态之前，将其视为待处理，而不是失败。

## 相关内容

> **[Mobile Integration Guide](/developer-resources/mobile-integration)**
>
> 移动端结账流程的最佳实践。

> **[Kotlin SDK](/developer-resources/sdks/kotlin)**
>
> 用于服务器端操作的 Backend SDK。


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