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

# Flutter

> Mở hosted checkout của Dodo Payments từ ứng dụng Flutter trong chế độ xem trình duyệt hệ thống bằng package Flutter chính thức, và nhận kết quả checkout có kiểu dữ liệu từ một lần gọi.

> [!NOTE]
>
> Trang này trình bày package Flutter chính thức của Dodo Payments, `dodopayments_checkout` trên pub.dev. Ngoài ra còn có một package do cộng đồng xây dựng. Xem
> [Dự án cộng đồng](/community/projects).

> **[Checkout Sessions API](/developer-resources/checkout-session)**
>
> Tạo `checkout_url` mà SDK này sẽ mở từ backend của bạn.

> **[Mobile Integration Guide](/developer-resources/mobile-integration)**
>
> Tìm hiểu cách SDK này tích hợp vào toàn bộ quy trình thanh toán trên mobile.

`dodopayments_checkout` mở hosted checkout của Dodo Payments trong `SFSafariViewController` trên iOS và trong Custom Tab trên Android, đồng thời trả về `CheckoutResult` có kiểu dữ liệu. Nó sử dụng cùng native code với các SDK [iOS](/developer-resources/sdks/ios) và
[Android](/developer-resources/sdks/android) độc lập, và toàn bộ logic checkout nằm trong native code đó. Lớp Dart truyền mỗi lần gọi qua channel [Pigeon](https://pub.dev/packages/pigeon) có kiểu dữ liệu. Package không chứa API key và không bao giờ gọi Dodo Payments API.

**Yêu cầu:** Flutter 3.44 trở lên với Dart 3.12 trở lên, iOS 16 trở lên và Android `minSdk` 23.

## Cài đặt

**1. Add the Dependency**

Thêm package vào `pubspec.yaml`:

```yaml pubspec.yaml theme={null}
dependencies:
  dodopayments_checkout: ^1.1.0
```

[Tùy chỉnh giao diện](#appearance-customization) yêu cầu phiên bản 1.1.0 trở lên.

Plugin Android biên dịch dựa trên Android SDK 35 theo mặc định. Nếu plugin khác yêu cầu `compileSdk` cao hơn, hãy đặt `dodoCompileSdk` trong `gradle.properties` của ứng dụng.

**2. Register a Callback URL Scheme**

Đăng ký một URL scheme để hệ điều hành định tuyến URL quay lại của checkout về ứng dụng. Sử dụng scheme này trong `returnUrl` bạn truyền cho SDK, và đặt cùng URL đó làm `return_url` của checkout session khi backend tạo session. URL không cần tải một trang thực tế.

**iOS**

Thêm một URL type cho scheme của bạn trong `ios/Runner/Info.plist`:

```xml ios/Runner/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>
```

`SFSafariViewController` không thể tự bắt URL quay lại của mình, vì vậy iOS sẽ mở URL trong ứng dụng của bạn. Chuyển tiếp mọi URL đến SDK, chẳng hạn từ
[`app_links`](https://pub.dev/packages/app_links):

```dart theme={null}
import 'package:dodopayments_checkout/dodopayments_checkout.dart';

// url is the incoming link as a String. For a Uri from app_links, pass uri.toString().
DodoCheckout.instance.handleOpenURL(url);
```

> [!NOTE]
>
> Bạn có thể chuyển tiếp mọi URL. `handleOpenURL` chỉ xử lý URL khớp với `returnUrl` của checkout đang diễn ra và resolve `true` cho URL đó. Với mọi URL khác, nó resolve `false`. Trên Android, nó luôn resolve `false`.

**Android**

Đặt callback scheme làm Gradle manifest placeholder trong `android/app/build.gradle.kts`. Dòng tương tự cũng hoạt động trong tệp `build.gradle` Groovy:

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

> [!WARNING]
>
> Nếu `MainActivity` đặt `android:taskAffinity=""` (mặc định trong template `flutter create`), hãy xóa nó hoặc đặt cùng affinity cho các activity của SDK. Nếu không, một số bản dựng Android của OEM có thể làm mất checkout đang diễn ra và thất bại với `PLATFORM_ERROR` cùng thông báo "Checkout was launched without its parameters."

## Cách sử dụng

Gọi `DodoCheckout.instance.start` với `checkout_url` từ backend của bạn:

```dart theme={null}
import 'package:dodopayments_checkout/dodopayments_checkout.dart';

// showSuccess(), showFailure(), showExpired(), and reconcileAbandonedSession()
// are your own functions. See Abandoned Sessions for what to reconcile.
final result = await DodoCheckout.instance.start(
  CheckoutParams(
    checkoutUrl: Uri.parse(checkoutUrl), // from your backend's checkout session
    returnUrl: Uri.parse('myapp://checkout/return'), // scheme must be registered (see Installation)
    onEvent: (event) => print(event.type), // logging only
  ),
);

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

`onEvent` nhận các event có `type` là `CheckoutEventType.opened`, `returnReceived` hoặc `closed`. Chỉ sử dụng chúng để logging, không bao giờ dùng để quyết định kết quả.

## Ý nghĩa của kết quả

SDK xây dựng `CheckoutResult` từ các query parameter trên URL quay lại.

> [!WARNING]
>
> `result.status` là gợi ý UI, không phải bằng chứng thanh toán. Xác nhận mọi khoản thanh toán từ backend của bạn bằng webhook `payment.succeeded` hoặc `subscription.active`.

**status** `CheckoutStatus` *required*

Một trong năm giá trị:

* `succeeded`: URL quay lại có `status=succeeded` (thanh toán một lần) hoặc `status=active` (subscription).
* `failed`: khoản thanh toán bị từ chối (`status=failed`).
* `cancelled`: khách hàng đã đóng chế độ xem trình duyệt trước khi URL quay lại xuất hiện. SDK không biết kết quả và khoản thanh toán có thể đã thành công, vì vậy không hiển thị màn hình thất bại. Thay vào đó, hãy đối soát [abandoned session](#abandoned-sessions).
* `pending`: khoản thanh toán được hoàn tất sau đó (`status=processing` hoặc bất kỳ giá trị `requires_*` nào), hoặc parameter `status` bị thiếu hoặc không được nhận diện. Đối soát giống như `cancelled`.
* `expired`: checkout session đã hết hạn (`status=expired`).

**paymentId** `String?`

Query parameter `payment_id`, khi URL quay lại có parameter này. Hiển thị parameter trong UI, nhưng không dùng nó để cấp quyền truy cập. Xem [Xác minh khoản thanh toán](#verify-the-payment).

**subscriptionId** `String?`

Query parameter `subscription_id`. Được đặt cho các checkout subscription.

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

Query parameter `license_key`. Được đặt khi checkout bao gồm các sản phẩm có license key.

**customerEmail** `String?`

Query parameter `email`. Được đặt khi checkout thu thập địa chỉ email.

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

Mọi query parameter từ URL quay lại, giữ nguyên văn.

## Xác minh khoản thanh toán

> **[Webhooks](/developer-resources/webhooks)**
>
> Dodo Payments gọi backend của bạn khi khoản thanh toán thành công hoặc subscription được kích hoạt.

> **[Get Payment Detail](/api-reference/payments/get-payments-1)**
>
> Tra cứu `paymentId` bằng secret key của bạn để kiểm tra trạng thái.

Chỉ cấp quyền truy cập sau khi một trong các cách này xác nhận khoản thanh toán. Không chỉ dựa vào `result.status`.

## Tùy chỉnh giao diện

Để thay đổi toolbar, button và bảng màu của trình duyệt checkout, truyền một `BrowserCustomization` làm `customization` trên `CheckoutParams`. Android Custom Tabs và iOS `SFSafariViewController` cung cấp các native control khác nhau, vì vậy các tùy chọn được chia thành `AndroidBrowserOptions` và `IosBrowserOptions`. Mỗi platform bỏ qua các tùy chọn của platform còn lại. Mọi field đều không bắt buộc và mặc định là `null`. Với field `null`, SDK không đặt tùy chọn đó và platform áp dụng mặc định riêng.

> **Android — Custom Tab**
>
> **toolbarColor** `Color?`
>
> Màu nền toolbar.
>
> **navigationBarColor** `Color?`
>
> Màu navigation bar.
>
> **navigationBarDividerColor** `Color?`
>
> Màu của đường phân cách phía trên navigation bar.
>
> **closeButtonStyle** `CloseButtonStyle?`
>
> `standard` hiển thị biểu tượng "X" của hệ thống. `back` hiển thị mũi tên quay lại do SDK vẽ.
>
> **closeButtonPosition** `CloseButtonPosition?`
>
> Vị trí trên toolbar nơi nút đóng xuất hiện: `start` hoặc `end`.
>
> **shareButtonEnabled** `bool?`
>
> Hiển thị biểu tượng chia sẻ của toolbar. `false` ẩn biểu tượng này.
>
> **showTitleEnabled** `bool?`
>
> Hiển thị tiêu đề trang bên dưới URL trong toolbar.
>
> **urlBarHidingEnabled** `bool?`
>
> Tự động ẩn toolbar khi trang được cuộn.
>
> **bookmarksButtonEnabled** `bool?`
>
> Hiển thị "Bookmark this page" trong menu overflow.
>
> **downloadsButtonEnabled** `bool?`
>
> Hiển thị "Download page" trong menu overflow.
>
> **colorScheme** `BrowserColorScheme?`
>
> `light` hoặc `dark` buộc giao diện đó bất kể cài đặt hệ thống của thiết bị. `system` tuân theo cài đặt hệ thống.

> **iOS — SFSafariViewController**
>
> **dismissButtonStyle** `DismissButtonStyle?`
>
> Kiểu của nút dismiss: `done`, `close` hoặc `cancel`. iOS quyết định hiển thị dưới dạng label hay icon.
>
> **presentationStyle** `PresentationStyle?`
>
> `pageSheet` (được dùng khi bạn để `null`) hiển thị một card mà khách hàng có thể vuốt xuống để dismiss. `fullScreen` phủ toàn bộ màn hình.
>
> **barCollapsingEnabled** `bool?`
>
> Cho phép toolbar thu gọn khi trang được cuộn. Tùy chọn này chỉ có hiệu lực khi `presentationStyle` là `fullScreen`. Với `pageSheet`, các thanh luôn được ghim bất kể cài đặt này.
>
> **colorScheme** `BrowserColorScheme?`
>
> `light` hoặc `dark` buộc giao diện đó bất kể cài đặt hệ thống của thiết bị. `system` tuân theo cài đặt hệ thống.

iOS không có tùy chọn màu toolbar vì các thuộc tính tint của `SFSafariViewController` bên dưới đã bị deprecated kể từ iOS 26.

```dart theme={null}
import 'package:dodopayments_checkout/dodopayments_checkout.dart';
import 'package:flutter/material.dart' show Color;

final result = await DodoCheckout.instance.start(
  CheckoutParams(
    checkoutUrl: Uri.parse(checkoutUrl),
    returnUrl: Uri.parse('myapp://checkout/return'),
    customization: BrowserCustomization(
      android: AndroidBrowserOptions(
        toolbarColor: Color(0xFF6366F1),
        closeButtonStyle: CloseButtonStyle.back,
      ),
      ios: IosBrowserOptions(
        presentationStyle: PresentationStyle.fullScreen,
        colorScheme: BrowserColorScheme.dark,
      ),
    ),
  ),
);
```

## Lỗi

`start` chỉ throw `CheckoutException` khi sử dụng sai hoặc platform failure. Đọc lý do từ `code`, một `CheckoutErrorCode`. Chuỗi native code nằm trong `nativeCode`.
Khách hàng hủy hoặc khoản thanh toán bị từ chối luôn là một result, không phải exception.

* `invalidCheckoutUrl` (`INVALID_CHECKOUT_URL`): `checkoutUrl` không phải là checkout session URL `https` (path bắt đầu bằng `/session/`) trên `checkout.dodopayments.com` hoặc `test.checkout.dodopayments.com`.
* `invalidReturnUrl` (`INVALID_RETURN_URL`): `returnUrl` không phải URL tuyệt đối có scheme và host.
* `alreadyInProgress` (`ALREADY_IN_PROGRESS`): một checkout khác đang chạy. Mỗi lần chỉ có thể chạy một checkout.
* `platformError` (`PLATFORM_ERROR`): platform failure không mong đợi. Các native error không xác định cũng được ánh xạ đến code này.

## Abandoned session

> [!NOTE]
>
> Native SDK ghi lại checkout session khi checkout bắt đầu và chỉ xóa bản ghi khi checkout kết thúc với `succeeded`, `failed` hoặc `expired`. Bản ghi vẫn còn khi ứng dụng bị tắt trong lúc checkout và sau result `cancelled` hoặc `pending`. Kiểm tra bản ghi này ở lần khởi chạy tiếp theo và sau mỗi result `cancelled` hoặc `pending`.

```dart theme={null}
import 'package:dodopayments_checkout/dodopayments_checkout.dart';

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

`abandoned.sessionId` là ID của checkout session, bắt đầu bằng `cks_`. `abandoned.createdAt` là thời điểm `DateTime` checkout bắt đầu. Backend của bạn có thể tra cứu session bằng [Get Checkout Session](/api-reference/checkout-sessions/get-checkouts), trả về `payment_id` và `payment_status`. Cho đến khi khoản thanh toán đạt trạng thái cuối cùng, hãy coi khoản thanh toán là đang chờ xử lý, không phải thất bại.

## Liên quan

> **[Mobile Integration Guide](/developer-resources/mobile-integration)**
>
> Cùng một contract cho Android, iOS và React Native.

> **[Community Projects](/community/projects)**
>
> Ngoài ra còn có một package Flutter do cộng đồng xây dựng.


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