Dodo Payments Checkout Flutter SDK

Open Dodo Payments' hosted checkout in a system browser tab (SFSafariViewController on iOS, a Custom Tab on Android) and get a clean result from one call.

This package is a thin wrapper: all checkout logic lives in the native iOS (DodoCheckout, Swift) and Android (com.dodopayments.api:checkout-android) cores. The Dart layer passes the call through a typed Pigeon channel and maps the result back.

Install

dependencies:
  dodopayments_checkout: ^1.0.0

Requirements: Flutter 3.44+ / Dart 3.12+, iOS 16+, Android minSdk 23.

Setup

Register a callback URL scheme so the OS routes the checkout return back to your app:

  • iOS: add a URL type for your scheme in Info.plist, and forward incoming URLs (e.g. via app_links) into DodoCheckout.instance.handleOpenURL(url)SFSafariViewController has no in-process way to catch its own return URL.

  • Android: set your callback scheme as a Gradle manifest placeholder — the underlying Android core's own manifest already declares the redirect activity's intent-filter, so this one property is the entire setup cost:

    android {
        defaultConfig {
            manifestPlaceholders["dodoCallbackScheme"] = "myapp"
        }
    }
    

    If your MainActivity sets android:taskAffinity="" (the stock flutter create template default, meant to stop the launcher icon from resurrecting a stale engine instance), remove it or give the SDK's activities the same explicit affinity. The SDK's own activities inherit the app's default task affinity, and leaving MainActivity on a different one is a real mismatch — some OEM Android skins (observed on Vivo) can then treat a long-lived Custom Tab's return as belonging to a different task and recreate the SDK's host activity from scratch, losing the in-flight checkout and surfacing PLATFORM_ERROR: Checkout was launched without its parameters.

Use

import 'package:dodopayments_checkout/dodopayments_checkout.dart';

final result = await DodoCheckout.instance.start(
  CheckoutParams(
    checkoutUrl: Uri.parse(checkoutUrl), // from your backend's POST /checkouts
    returnUrl: Uri.parse('myapp://checkout/return'), // scheme must be registered (see Setup)
    onEvent: (event) => print(event.type), // logging only — never decide outcome from events
  ),
);

switch (result.status) {
  case CheckoutStatus.succeeded: showSuccess(result.paymentId); // UI only — confirm server-side
  case CheckoutStatus.failed:    showFailure();
  case CheckoutStatus.cancelled: await reconcileAbandonedSession(); // outcome unknown — NOT a failure
  case CheckoutStatus.pending:   await reconcileAbandonedSession(); // may be unparsed, not just async
  case CheckoutStatus.expired:   showExpired();
}

Your backend creates the checkout session (with your secret key) and sends the checkout_url to the app. Set the session's return_url to the same URL you pass as returnUrl — it never has to resolve, because the SDK cancels the navigation before it loads.

What the result means

The result comes from the return_url query string. It is a UI hint, not proof of payment. This SDK never calls the Dodo API and holds no API key. Grant access on your backend from the webhook (payment.succeeded / subscription.active) or by retrieving the payment with your secret key. result.raw carries every query parameter verbatim.

Verify the payment

Confirm every payment from your backend, not from the mobile result:

  • Webhook: Dodo Payments calls your backend when a payment succeeds or a subscription activates. Check the Webhooks guide.
  • Verification API: look up paymentId with your secret key via Get Payment Detail.

cancelled is not a failure

CheckoutStatus.cancelled means the user dismissed the browser before any return URL arrived, so the SDK never learned the outcome. The payment may have gone through. A user who pays and then taps ✕ while the hosted "Payment Successful" page counts down its redirect produces cancelled, and is indistinguishable, from the SDK's side, from a user who closed the browser without paying.

Showing "Payment failed" here tells a paying customer their money vanished. Resolve it instead: the SDK keeps the session on record for exactly this case.

Abandoned sessions

A session stays on record whenever the SDK never saw a return URL it could resolve to a durable outcome — the app was killed mid-checkout, start completed with CheckoutStatus.cancelled, or it completed with CheckoutStatus.pending from an unparseable return URL rather than a genuinely async payment method. Reconcile it server-side, both on next launch and right after a cancelled or pending result:

import 'package:dodopayments_checkout/dodopayments_checkout.dart';

Future<void> reconcileAbandonedSession() async {
  final abandoned = await DodoCheckout.instance.getAbandonedSession();
  if (abandoned == null) {
    dismiss(); // nothing in flight
    return;
  }
  // Ask *your* backend what happened to abandoned.sessionId — it has the
  // webhook (`payment.succeeded`) or can call Get Payment Detail with your
  // secret key. Show a spinner while you wait; an async method may still be
  // settling, so treat "no record yet" as pending, not failed — and only
  // clear the record once you have a terminal outcome, or a later retry
  // has nothing left to reconcile against if this one comes back.
  final outcome = await myBackend.outcomeForSession(abandoned.sessionId);
  if (outcome.isTerminal) {
    await DodoCheckout.instance.clearAbandonedSession();
  }
  show(outcome);
}

Customization

The checkout browser's toolbar, buttons, and color scheme can be customized via customization on CheckoutParams. See the Appearance Customization docs for all available options.

Errors

start throws CheckoutException only for misuse or platform failure. The code is a CheckoutErrorCode:

Code Native code Meaning
invalidCheckoutUrl INVALID_CHECKOUT_URL Not a checkout.dodopayments.com / test.checkout.dodopayments.com session URL
invalidReturnUrl INVALID_RETURN_URL Not a valid absolute URL
alreadyInProgress ALREADY_IN_PROGRESS A checkout is already running (only one at a time)
platformError PLATFORM_ERROR Unexpected platform failure

A user cancelling or a declined payment is a result (CheckoutStatus.cancelled / CheckoutStatus.failed), never an exception.

Example

example/lib/main.dart demonstrates a full price-page flow. Generate its platform folders once with flutter create --platforms=android,ios . inside example/ and run it.

Development notes

  • Platform channels are generated by Pigeon from pigeons/messages.dart. After editing that file: dart run pigeon --input pigeons/messages.dart.
  • iOS bundles the Swift core as source (see ios/dodopayments_checkout.podspec); Android depends on the com.dodopayments.api:checkout-android Maven artifact (see android/build.gradle for local-dev pointers to the sibling cores in this monorepo).

Libraries

dodopayments_checkout
Dodo Payments mobile checkout for Flutter.