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

> Buka checkout hosted Dodo Payments dari aplikasi Flutter dalam tampilan browser sistem dengan package Flutter resmi, dan dapatkan hasil checkout bertipe dari satu panggilan.

<Info>
  Halaman ini membahas package Flutter resmi Dodo Payments, `dodopayments_checkout` di pub.dev. Package terpisah yang dibuat komunitas juga tersedia. Lihat
  [Proyek Komunitas](/community/projects).
</Info>

<CardGroup cols={2}>
  <Card title="Checkout Sessions API" icon="cart-shopping" href="/developer-resources/checkout-session">
    Buat `checkout_url` yang dibuka SDK ini dari backend Anda.
  </Card>

  <Card title="Mobile Integration Guide" icon="mobile" href="/developer-resources/mobile-integration">
    Lihat cara SDK ini terintegrasi dengan alur pembayaran seluler secara menyeluruh.
  </Card>
</CardGroup>

`dodopayments_checkout` membuka checkout hosted Dodo Payments dalam `SFSafariViewController` di iOS dan dalam Custom Tab di Android, lalu mengembalikan `CheckoutResult` bertipe. Package ini menggunakan native code yang sama dengan SDK [iOS](/developer-resources/sdks/ios) dan
[Android](/developer-resources/sdks/android) mandiri, dan seluruh logika checkout berada dalam native code tersebut. Lapisan Dart meneruskan setiap panggilan melalui channel
[Pigeon](https://pub.dev/packages/pigeon) bertipe. Package ini tidak menyimpan API key dan tidak pernah memanggil API Dodo Payments.

**Persyaratan:** Flutter 3.44 atau lebih baru dengan Dart 3.12 atau lebih baru, iOS 16 atau lebih baru, dan Android `minSdk` 23.

## Instalasi

<Steps>
  <Step title="Add the Dependency">
    Tambahkan package ke `pubspec.yaml`:

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

    [Kustomisasi tampilan](#appearance-customization) memerlukan versi 1.1.0 atau lebih baru.

    Plugin Android dikompilasi menggunakan Android SDK 35 secara default. Jika plugin lain memerlukan `compileSdk` yang lebih tinggi, tetapkan `dodoCompileSdk` di `gradle.properties` aplikasi Anda.
  </Step>

  <Step title="Register a Callback URL Scheme">
    Daftarkan skema URL agar sistem operasi mengarahkan URL pengembalian checkout kembali ke aplikasi Anda. Gunakan skema ini dalam `returnUrl` yang Anda teruskan ke SDK, dan tetapkan URL yang sama sebagai `return_url` pada checkout session saat backend Anda membuat session tersebut. URL tersebut tidak perlu memuat halaman nyata.

    <Tabs>
      <Tab title="iOS">
        Tambahkan tipe URL untuk skema Anda di `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` tidak dapat menangkap URL pengembaliannya sendiri, sehingga iOS membuka URL tersebut di aplikasi Anda. Teruskan setiap URL yang masuk ke SDK, misalnya dari
        [`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>
          Anda dapat meneruskan setiap URL. `handleOpenURL` hanya bertindak pada URL yang cocok dengan
          `returnUrl` dari checkout yang sedang berlangsung, dan menyelesaikan `true` untuk URL tersebut. Untuk URL
          lainnya, `false` akan diselesaikan. Di Android, `false` selalu diselesaikan.
        </Note>
      </Tab>

      <Tab title="Android">
        Tetapkan skema callback Anda sebagai manifest placeholder Gradle di `android/app/build.gradle.kts`. Baris yang sama berfungsi dalam file `build.gradle` Groovy:

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

        <Warning>
          Jika `MainActivity` menetapkan `android:taskAffinity=""` (default dalam template
          `flutter create`), hapus pengaturan tersebut atau berikan affinity yang sama kepada activity SDK. Jika tidak, beberapa build Android OEM dapat kehilangan checkout yang sedang berlangsung
          dan gagal dengan `PLATFORM_ERROR` serta pesan "Checkout was launched without its parameters."
        </Warning>
      </Tab>
    </Tabs>
  </Step>
</Steps>

## Penggunaan

Panggil `DodoCheckout.instance.start` dengan `checkout_url` dari backend Anda:

```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` menerima event yang `type`-nya adalah `CheckoutEventType.opened`, `returnReceived`, atau `closed`. Gunakan event tersebut hanya untuk logging, jangan pernah untuk menentukan hasil.

## Arti Hasil

SDK membuat `CheckoutResult` dari parameter query pada URL pengembalian.

<Warning>
  `result.status` adalah petunjuk UI, bukan bukti pembayaran. Konfirmasikan setiap pembayaran
  dari backend Anda, menggunakan webhook `payment.succeeded` atau `subscription.active`.
</Warning>

<ParamField body="status" type="CheckoutStatus" required>
  Salah satu dari lima nilai:

  * `succeeded`: URL pengembalian memiliki `status=succeeded` (pembayaran satu kali) atau `status=active` (subscription).
  * `failed`: pembayaran ditolak (`status=failed`).
  * `cancelled`: customer menutup tampilan browser sebelum URL pengembalian tiba. SDK tidak mengetahui hasilnya, dan pembayaran mungkin berhasil, jadi jangan tampilkan layar kegagalan. Rekonsiliasikan [session yang ditinggalkan](#abandoned-sessions) sebagai gantinya.
  * `pending`: pembayaran diselesaikan nanti (`status=processing` atau nilai `requires_*` apa pun), atau parameter `status` tidak ada atau tidak dikenali. Rekonsiliasikan seperti `cancelled`.
  * `expired`: checkout session kedaluwarsa (`status=expired`).
</ParamField>

<ParamField body="paymentId" type="String?">
  Parameter query `payment_id`, jika URL pengembalian menyertakannya. Tampilkan di UI, tetapi jangan gunakan untuk memberikan akses. Lihat [Verifikasi Pembayaran](#verify-the-payment).
</ParamField>

<ParamField body="subscriptionId" type="String?">
  Parameter query `subscription_id`. Ditetapkan untuk checkout subscription.
</ParamField>

<ParamField body="licenseKeys" type="List<String>?">
  Parameter query `license_key`. Ditetapkan ketika checkout menyertakan produk license key.
</ParamField>

<ParamField body="customerEmail" type="String?">
  Parameter query `email`. Ditetapkan ketika checkout mengambil alamat email.
</ParamField>

<ParamField body="raw" type="Map<String, String>">
  Setiap parameter query dari URL pengembalian, secara verbatim.
</ParamField>

## Verifikasi Pembayaran

<CardGroup cols={2}>
  <Card title="Webhooks" icon="webhook" href="/developer-resources/webhooks">
    Dodo Payments memanggil backend Anda ketika pembayaran berhasil atau subscription aktif.
  </Card>

  <Card title="Get Payment Detail" icon="magnifying-glass" href="/api-reference/payments/get-payments-1">
    Cari `paymentId` menggunakan secret key Anda untuk memeriksa statusnya.
  </Card>
</CardGroup>

Berikan akses hanya setelah salah satu hal berikut mengonfirmasi pembayaran. Jangan hanya mengandalkan
`result.status`.

## Kustomisasi Tampilan

Untuk mengubah toolbar, tombol, dan skema warna browser checkout, teruskan `BrowserCustomization` sebagai `customization` pada `CheckoutParams`. Android Custom Tabs dan `SFSafariViewController` iOS mengekspos kontrol native yang berbeda, sehingga opsinya dibagi menjadi `AndroidBrowserOptions` dan `IosBrowserOptions`. Setiap platform mengabaikan opsi milik platform lainnya. Semua field bersifat opsional dan secara default bernilai `null`. Untuk field `null`, SDK tidak menetapkan opsi tersebut dan platform menerapkan default-nya sendiri.

<AccordionGroup>
  <Accordion title="Android — Custom Tab">
    <ParamField body="toolbarColor" type="Color?">
      Warna latar toolbar.
    </ParamField>

    <ParamField body="navigationBarColor" type="Color?">
      Warna navigation bar.
    </ParamField>

    <ParamField body="navigationBarDividerColor" type="Color?">
      Warna pemisah di atas navigation bar.
    </ParamField>

    <ParamField body="closeButtonStyle" type="CloseButtonStyle?">
      `standard` menampilkan ikon "X" sistem. `back` menampilkan panah kembali yang digambar oleh SDK.
    </ParamField>

    <ParamField body="closeButtonPosition" type="CloseButtonPosition?">
      Sisi toolbar tempat tombol tutup muncul: `start` atau `end`.
    </ParamField>

    <ParamField body="shareButtonEnabled" type="bool?">
      Menampilkan ikon bagikan pada toolbar. `false` menyembunyikannya.
    </ParamField>

    <ParamField body="showTitleEnabled" type="bool?">
      Menampilkan judul halaman di bawah URL pada toolbar.
    </ParamField>

    <ParamField body="urlBarHidingEnabled" type="bool?">
      Menyembunyikan toolbar secara otomatis saat halaman digulir.
    </ParamField>

    <ParamField body="bookmarksButtonEnabled" type="bool?">
      Menampilkan "Bookmark this page" dalam menu overflow.
    </ParamField>

    <ParamField body="downloadsButtonEnabled" type="bool?">
      Menampilkan "Download page" dalam menu overflow.
    </ParamField>

    <ParamField body="colorScheme" type="BrowserColorScheme?">
      `light` atau `dark` memaksa tampilan tersebut terlepas dari pengaturan sistem perangkat. `system` mengikuti pengaturan sistem.
    </ParamField>
  </Accordion>

  <Accordion title="iOS — SFSafariViewController">
    <ParamField body="dismissButtonStyle" type="DismissButtonStyle?">
      Gaya tombol tutup: `done`, `close`, atau `cancel`. iOS menentukan apakah tombol tersebut ditampilkan sebagai label atau ikon.
    </ParamField>

    <ParamField body="presentationStyle" type="PresentationStyle?">
      `pageSheet` (digunakan ketika Anda membiarkan `null`) menampilkan kartu yang dapat di-swipe ke bawah oleh customer untuk ditutup. `fullScreen` mencakup seluruh layar.
    </ParamField>

    <ParamField body="barCollapsingEnabled" type="bool?">
      Memungkinkan toolbar diciutkan saat halaman digulir. Pengaturan ini hanya terlihat efeknya ketika `presentationStyle` bernilai `fullScreen`. Dengan `pageSheet`, bar tetap tersemat terlepas dari pengaturan ini.
    </ParamField>

    <ParamField body="colorScheme" type="BrowserColorScheme?">
      `light` atau `dark` memaksa tampilan tersebut terlepas dari pengaturan sistem perangkat. `system` mengikuti pengaturan sistem.
    </ParamField>
  </Accordion>
</AccordionGroup>

iOS tidak memiliki opsi warna toolbar karena properti tint `SFSafariViewController` yang mendasarinya tidak digunakan lagi sejak 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,
      ),
    ),
  ),
);
```

## Error

`start` hanya melempar `CheckoutException` jika terjadi penggunaan yang salah atau kegagalan platform. Baca alasannya dari `code`, sebuah `CheckoutErrorCode`. String native code berada di `nativeCode`.
Customer yang membatalkan, atau pembayaran yang ditolak, selalu merupakan hasil, bukan exception.

* `invalidCheckoutUrl` (`INVALID_CHECKOUT_URL`): `checkoutUrl` bukan URL checkout session `https` (path yang dimulai dengan `/session/`) di `checkout.dodopayments.com` atau `test.checkout.dodopayments.com`.
* `invalidReturnUrl` (`INVALID_RETURN_URL`): `returnUrl` bukan URL absolut dengan skema dan host.
* `alreadyInProgress` (`ALREADY_IN_PROGRESS`): checkout lain sedang berjalan. Hanya satu checkout yang dapat berjalan pada satu waktu.
* `platformError` (`PLATFORM_ERROR`): kegagalan platform yang tidak terduga. Native error yang tidak diketahui juga dipetakan ke kode ini.

## Session yang Ditinggalkan

<Info>
  Native SDK mencatat checkout session saat checkout dimulai, dan menghapus catatan tersebut hanya ketika checkout berakhir dengan `succeeded`, `failed`, atau `expired`. Catatan tersebut tetap ada ketika aplikasi dihentikan selama checkout, serta setelah hasil `cancelled` atau `pending`. Periksa catatan tersebut saat peluncuran berikutnya dan setelah setiap hasil `cancelled` atau `pending`.
</Info>

```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` adalah ID checkout session, yang dimulai dengan `cks_`. `abandoned.createdAt` adalah `DateTime` saat checkout dimulai. Backend Anda dapat mencari session tersebut menggunakan [Get Checkout Session](/api-reference/checkout-sessions/get-checkouts), yang mengembalikan `payment_id` dan `payment_status`. Hingga pembayaran mencapai status final, perlakukan pembayaran tersebut sebagai pending, bukan gagal.

## Terkait

<CardGroup cols={2}>
  <Card title="Mobile Integration Guide" icon="mobile" href="/developer-resources/mobile-integration">
    Kontrak yang sama untuk Android, iOS, dan React Native.
  </Card>

  <Card title="Community Projects" icon="users" href="/community/projects">
    Package Flutter terpisah yang dibuat komunitas juga tersedia.
  </Card>
</CardGroup>


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