Crossmint Flutter SDK

The Flutter SDK for integrating Crossmint wallets, authentication, checkout, and verifiable credentials into your mobile app
Explore the docs »

View Example App · Report Bug · Request Feature

Table of Contents
  1. About The Project
  2. Package Overview
  3. Getting Started
  4. Embedded Checkout
  5. Platform Setup
  6. Demo Applications
  7. Building the SDK Locally
  8. Generating API Reference Docs
  9. Documentation
  10. Known Limitations
  11. Contact

About The Project

The Crossmint Flutter SDK brings the full power of the Crossmint platform to Flutter mobile apps. It is the Flutter counterpart to the Crossmint SDK for React and React Native.

The SDK is headless-first: you own the app UI, while the SDK provides client surfaces, runtime wallet helpers, auth callback wiring, a hidden signer runtime, and native device-signer integration. Optional UI components are available for common flows like authentication forms and OTP prompts.

Why Crossmint?

  • Quick Integration: Get up and running in minutes with a single package dependency
  • Developer-First: Build end-to-end blockchain solutions without deep Web3 expertise
  • Secure: Enterprise-grade security with Secure Enclave / Android Keystore device signers
  • Fiat-First: Enable users to participate in Web3 without cryptocurrency via embedded checkout
  • Free to Start: Start developing at no cost

Core Features

  • Embedded wallets: Create and manage EVM, Solana, and Stellar wallets
  • Authentication: Email OTP, OAuth (Google, Twitter), wallet-based sign-in, and session management
  • Signers: Email, phone, device, external wallet, passkey, and server signers
  • Embedded checkout: Credit card and crypto payments via a drop-in WebView widget
  • Verifiable credentials: Issue, present, and verify on-chain credentials

Package Overview

Most apps should depend only on the main SDK package:

  • crossmint_flutter: The main Flutter SDK — auth, wallets, signers, checkout, and credentials.

The SDK is composed of several internal packages that you typically do not need to depend on directly:

  • crossmint_core: Pure Dart base abstractions (no Flutter or external dependencies)
  • crossmint_auth: Authentication domain logic
  • crossmint_wallets: Wallet domain logic
  • crossmint_verifiable_credentials: Verifiable credentials helpers
  • crossmint_device_signer: Native plugin for on-device key storage (Secure Enclave / Android Keystore)

Getting Started

Prerequisites

  • Flutter mobile app targeting iOS and/or Android
  • Flutter 3.41+ and Dart 3.11+
  • A Crossmint client key (get one here)
  • An app callback scheme for OAuth flows on mobile

Installation

Add the SDK to your project:

flutter pub add crossmint_flutter

Quickstart

A full integration is four steps: initialize the client, sign the user in, mount the wallet host, and load a wallet. OTP prompt handling is covered in a reference subsection afterwards.

1. Initialize the client

import 'package:flutter/widgets.dart';
import 'package:crossmint_flutter/crossmint_client.dart';
import 'package:crossmint_flutter/crossmint_flutter_auth.dart';
import 'package:crossmint_flutter/crossmint_flutter_controllers.dart';

late final CrossmintClient client;
late final CrossmintWalletController walletController;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  client = CrossmintClient(
    config: const CrossmintClientConfig(
      apiKey: String.fromEnvironment('CROSSMINT_API_KEY'),
      appScheme: String.fromEnvironment('CROSSMINT_APP_SCHEME'),
    ),
  );

  await client.initialize();
  await client.auth.restoreSession();

  final authRouter = CrossmintAuthCallbackRouter(auth: client.auth);
  await authRouter.start();

  walletController = client.createWalletController(
    CrossmintWalletControllerConfig(
      createOnLogin: CrossmintCreateOnLoginConfig(
        chain: CrossmintChain.baseSepolia.apiValue,
        recovery: const CrossmintEmailSignerConfig(),
      ),
    ),
  );

  runApp(MyApp(client: client, walletController: walletController));
}

restoreSession() hydrates a persisted JWT if a previous session is still valid, so returning users skip sign-in. createOnLogin (consumed by walletController.ensureLoaded() in step 4) tells the controller to create-or-fetch a wallet when it's invoked; it's not an auth-state listener on its own.

Recovery signer. CrossmintEmailSignerConfig() with no email means "use the signed-in user's email," which the controller reads from auth.getUser() during wallet creation. This assumes email-based sign-in (OTP or OAuth). If you use wallet sign-in — where the authenticated user may not have an email — either pass CrossmintEmailSignerConfig(email: '...') with an explicit address or choose a different recovery signer (e.g. an external-wallet signer); otherwise wallet creation throws CrossmintWalletException.

OTP UI. CrossmintWalletControllerConfig.showOtpSignerPrompt is inert plumbing at the moment — nothing in the runtime OTP path reads it. To render the default Material OTP modal, use CrossmintWalletProvider (from crossmint_flutter_ui.dart) instead of CrossmintWalletHost and pass otpPromptBuilder: crossmintDefaultOtpPromptBuilder on CrossmintWalletProviderConfig. In the headless CrossmintWalletHost flow shown here, drive OTP yourself via walletController.otp.challengeListenable (see OTP prompt handling).

2. Sign the user in

Email OTP — read back challenge.state and pass it as emailId when confirming:

final challenge = await client.auth.sendEmailOtp(email);
final ok = await client.auth.confirmEmailOtp(
  email: email,
  emailId: challenge.state ?? '',
  token: codeEnteredByUser,
);

OAuth — the redirect returns through the CrossmintAuthCallbackRouter started in step 1. This requires the deep-link entries in Platform Setup.

await client.auth.loginWithOAuth(CrossmintOAuthProvider.google);
// or CrossmintOAuthProvider.twitter

Wallet sign-in (SIWE / SIWS) is also supported via client.auth.signInWithWallet(address, type) — see doc/architecture/public-api-spec.md.

3. Mount the wallet host

Wrap your app tree in CrossmintWalletHost so the hidden signer WebView bridge has a place to run:

class MyApp extends StatelessWidget {
  const MyApp({super.key, required this.client, required this.walletController});

  final CrossmintClient client;
  final CrossmintWalletController walletController;

  @override
  Widget build(BuildContext context) {
    return CrossmintWalletHost(
      client: client,
      walletController: walletController,
      child: /* your app tree */,
    );
  }
}

Required for non-custodial signers. Email and phone signers run inside a hidden WebView that the SDK manages for you. CrossmintWalletHost (headless, re-exported from crossmint_flutter_controllers.dart) — or CrossmintWalletProvider from crossmint_flutter_ui.dart if you want the opt-in default UI — must be mounted in the widget tree. Without one, wallet.useSigner(CrossmintEmailSignerConfig(...)) throws CrossmintSignerException(code: nonCustodialBridgeHostNotMounted). Passkey signers sign via onSignWithPasskey callbacks and do not require a host.

4. Load or create a wallet

Loading is a two-step process: (a) resolve a CrossmintWallet into walletController.currentWallet (a data model), then (b) wrap it in a runtime wallet that can sign and send transactions. End-to-end:

await walletController.ensureLoaded();                 // a. resolve currentWallet
final wallet = walletController.createEvmWallet();     // b. wrap as runtime EVM wallet
await wallet.useSigner(CrossmintEmailSignerConfig(email: signedInEmail));
await wallet.sendToken('0xRecipient', 'usdc', '1.0');

The subsections below break each step down.

Step a — resolve currentWallet. Pick by user journey:

  • ensureLoaded() + createOnLogin. Configure createOnLogin (see step 1) and call walletController.ensureLoaded() yourself after a successful sign-in. The controller creates-or-fetches a wallet matching the config. (If you use CrossmintWalletProvider from crossmint_flutter_ui.dart instead of the headless CrossmintWalletHost, the provider invokes ensureLoaded() for you on auth-state change when CrossmintWalletProviderConfig.autoLoadWallet is true — the default.)
  • Explicit fetch / create — for returning users or multi-wallet apps. Fetch by chain:
await walletController.getWallet(
  CrossmintWalletLookupRequest(chain: CrossmintChain.baseSepolia.apiValue),
);

Or create explicitly with a recovery signer:

await walletController.createWallet(
  CrossmintWalletCreateRequest(
    chain: CrossmintChain.baseSepolia.apiValue,
    recovery: const CrossmintEmailSignerConfig(),
  ),
);
  • Runtime-only facade — for apps that manage wallet lifecycle themselves (no controller state). The facade returns a typed runtime wallet directly, skipping the data-model / runtime split. Signer configs must be fully resolved (pass an explicit email; no auth.getUser() lookup happens here). OTP prompts come through onAuthRequired — parameter semantics in the subsection below:
final wallets = CrossmintWallets.from(
  client,
  onAuthRequired: (signerType, signerLocator, needsAuth, sendOtp, verifyOtp, reject) async {
    if (!needsAuth) return;
    await sendOtp();
    final code = await showMyOtpDialog();
    await verifyOtp(code);
  },
);
final wallet = await wallets.createWallet(
  chain: CrossmintChain.baseSepolia.apiValue,
  recovery: const CrossmintEmailSignerConfig(email: 'user@example.com'),
);
await wallet.useSigner(const CrossmintEmailSignerConfig(email: 'user@example.com'));
await wallet.sendToken('0xRecipient', 'usdc', '1.0');

Step b — build a runtime wallet. walletController.currentWallet is a CrossmintWallet data model; transaction APIs (useSigner, sendToken, sendTransaction, …) live on the runtime wallet types. Wrap the loaded model with one of the controller's builders:

final wallet = walletController.createEvmWallet();
await wallet.useSigner(CrossmintEmailSignerConfig(email: signedInEmail));
await wallet.sendToken('0xRecipient', 'usdc', '1.0');

Use createSolanaWallet() / createStellarWallet() for other chain families, or one of the signer-bundled conveniences when you want the signer attached at construction:

  • createEvmWalletWithDeviceSigner — hardware-backed device keys (Secure Enclave / Android Keystore).
  • createEvmWalletWithNonCustodialSigner — email / phone signer backed by the hidden WebView bridge.
  • createEvmWalletWithExternalWalletSigner — user-supplied sign callbacks (e.g. an in-app external-wallet adapter).
  • createEvmWalletWithPasskeySigner — passkey signer via host-provided callbacks.

All throw CrossmintWalletException if no wallet is loaded.

Chain values. CrossmintChain (in packages/crossmint_core/lib/src/models/crossmint_chain.dart) enumerates every supported chain (EVM mainnets + testnets, Solana, Stellar) and its apiValue. Use CrossmintChain.fromApiValue('...') for safe parsing from dynamic strings, and tryFromApiValue(...) for a nullable return.

OTP prompt handling

When a non-custodial email or phone signer needs authentication, the SDK raises an OTP challenge. Two handling paths:

  • Controller-driven (recommended). Observe walletController.otp.challengeListenable (a ValueListenable<CrossmintOtpChallenge?>) and render your own prompt, or pass otpPromptBuilder: crossmintDefaultOtpPromptBuilder (from crossmint_flutter_ui.dart) to CrossmintWalletProviderConfig for the Material default modal.
  • Headless callback. When using CrossmintWallets.from(...) directly, supply onAuthRequired:
Param Meaning
signerType "email" or "phone"
signerLocator The locator string (e.g. email address or E.164 phone)
needsAuth true when the challenge opens, false when the SDK resolves it
sendOtp() Asks the Crossmint API to (re)send the code
verifyOtp(code) Submits the code the user entered
reject() Cancels the challenge

For more details, see the Crossmint documentation.

Embedded Checkout

The SDK includes CrossmintEmbeddedCheckout, a drop-in WebView widget for credit card and crypto payments. It supports fiat and crypto payment methods, custom theming, and can delegate crypto signing to a runtime wallet:

import 'package:crossmint_flutter/crossmint_flutter_ui.dart';
import 'package:crossmint_flutter/crossmint_flutter_wallets.dart';

final config = CrossmintCheckoutConfig(
  order: CrossmintCheckoutNewOrder.typed(
    lineItems: [
      CrossmintCheckoutLineItem.collection(
        collectionLocator: 'crossmint:your-collection-id',
        callData: const {'quantity': 1},
      ),
    ],
  ),
  payment: CrossmintCheckoutPayment(
    crypto: CrossmintCheckoutCryptoPayment(
      enabled: true,
      payer: CrossmintEvmWalletCheckoutPayer(
        wallet: wallet,
        supportedChains: const ['base-sepolia'],
      ),
    ),
    defaultMethodType: CrossmintCheckoutDefaultMethod.crypto,
  ),
);

// In your widget tree:
CrossmintEmbeddedCheckout(
  apiKey: 'ck_staging_...',
  config: config,
  onOrderUpdated: (order) => debugPrint('Order: ${order['orderId']}'),
);

CrossmintEvmWalletCheckoutPayer requires a runtime CrossmintEvmWallet (not the CrossmintWallet data model) — build it with walletController.createEvmWallet() (or one of the signer-bundled conveniences) per step 4b of the Quickstart. onOrderUpdated receives a Map<String, Object?>, so read fields with order['key']. Pass supportedChains as API-value strings (e.g. CrossmintChain.baseSepolia.apiValue).

EVM-only crypto payer for now. CrossmintEvmWalletCheckoutPayer is currently the only bundled crypto payer; Solana and Stellar runtime wallets don't have equivalents yet. Fiat-only flows work across all chains.

Widget reference

Param Type Purpose
apiKey String Crossmint client API key. Required.
config CrossmintCheckoutConfig Order, payment, and appearance config. Required.
payer CrossmintCheckoutPayer? Crypto payer override. Takes precedence over config.payment.crypto.payer.
checkoutController CrossmintCheckoutController? Reactive handle on order state (phase, quote, orderClientSecret).
onOrderUpdated void Function(Map<String, Object?>)? Fires on order state changes.
onOrderCreationFailed void Function(String)? Fires when the server rejects order creation.
onDiagnostic void Function(CrossmintCheckoutDiagnostic)? Runtime diagnostics (blocked navigation, malformed hosted messages, WebView errors).
loadingBuilder CrossmintCheckoutLoadingBuilder? Custom loading widget (typedef: Widget Function(BuildContext)). Defaults to a Material spinner; override for headless apps.
defaultHeight double Initial height in logical pixels before the hosted page reports its actual height. Defaults to 400.

Fiat-only checkout

For a pure credit-card / Apple Pay / Google Pay flow, omit the crypto payer and pass an email recipient so Crossmint mints into a managed wallet. Swap in CrossmintCheckoutWalletRecipient when minting directly to a user-owned external wallet address instead:

final config = CrossmintCheckoutConfig(
  order: CrossmintCheckoutNewOrder.typed(
    lineItems: [
      CrossmintCheckoutLineItem.collection(
        collectionLocator: 'crossmint:your-collection-id',
      ),
    ],
    recipient: const CrossmintCheckoutEmailRecipient(email: 'user@example.com'),
  ),
  payment: const CrossmintCheckoutPayment(
    fiat: CrossmintCheckoutFiatPayment(enabled: true, defaultCurrency: 'USD'),
    defaultMethodType: CrossmintCheckoutDefaultMethod.fiat,
  ),
);

Physical goods

For a merchandise-style line item, use CrossmintCheckoutPhysicalRecipient with a shipping address:

final config = CrossmintCheckoutConfig(
  order: CrossmintCheckoutNewOrder.typed(
    lineItems: [
      CrossmintCheckoutLineItem.product(productLocator: 'crossmint:tshirt'),
    ],
    recipient: CrossmintCheckoutPhysicalRecipient(
      email: 'user@example.com',
      physicalAddress: const CrossmintCheckoutPhysicalAddress(
        name: 'Jane Doe',
        line1: '123 Market St',
        city: 'San Francisco',
        state: 'CA',
        postalCode: '94103',
        country: 'US',
      ),
    ),
  ),
  payment: const CrossmintCheckoutPayment(
    fiat: CrossmintCheckoutFiatPayment(enabled: true),
  ),
);

Recipient types

Pick the recipient that matches the fulfillment shape of your line items:

Recipient Use when Required fields
CrossmintCheckoutEmailRecipient Minting to a Crossmint-managed wallet keyed to an email address. email
CrossmintCheckoutWalletRecipient Minting directly to a user-owned external wallet (EVM / Solana address). walletAddress
CrossmintCheckoutPhysicalRecipient Shipping physical goods — no mint, just fulfillment. email, physicalAddress

Any recipient may additionally carry a CrossmintCheckoutPhysicalAddress when a line item is hybrid (digital + physical).

Theming

Pass a CrossmintCheckoutAppearance on CrossmintCheckoutConfig(appearance: ...) to theme the hosted page. Three tiers: CSS custom properties, per-element rules, and web fonts.

final appearance = CrossmintCheckoutAppearance(
  variables: const <String, Object?>{
    'colorPrimary': '#0F172A',
    'borderRadius': '12px',
  },
  rules: const <String, Object?>{
    '.Input': {'borderColor': '#CBD5E1'},
    '.Tab': {'fontWeight': '600'},
  },
  fonts: const <Map<String, String>>[
    <String, String>{'cssSrc': 'https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap'},
  ],
);

The checkout_playground/ app has an interactive theming screen (checkout_playground/lib/screens/theme_screen.dart) you can use to dial in variables and rules before copying them into your app.

Observing diagnostics

onDiagnostic surfaces navigation blocks, malformed hosted messages, and WebView resource errors — wire it to your logger in staging / production. onOrderCreationFailed is the complementary hook for server-side order rejections:

CrossmintEmbeddedCheckout(
  apiKey: 'ck_staging_...',
  config: config,
  onDiagnostic: (diag) {
    debugPrint('[${diag.severity.name}] ${diag.code}: ${diag.message}');
  },
  onOrderCreationFailed: (message) {
    debugPrint('Order creation failed: $message');
  },
);

Platform Setup

OAuth sign-in and CrossmintAuthCallbackRouter rely on a deep-link scheme that matches the appScheme passed to CrossmintClientConfig. Register it on both platforms:

Android — in android/app/src/main/AndroidManifest.xml, inside your main <activity>:

<intent-filter>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data android:scheme="your-app-scheme"/>
</intent-filter>

iOS — in ios/Runner/Info.plist:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>your-app-scheme</string>
        </array>
    </dict>
</array>

The scheme string must match the appScheme you pass to CrossmintClientConfig. See example/android/ and example/ios/ for a working reference.

Demo Applications

  • Wallets Playground — the main example app with auth, wallets, signer management, and transaction flows. Supports multiple scenarios via environment config. See the playground feature checklist.
  • Checkout Playground — a standalone app for interactively testing the embedded checkout widget with real-time configuration: payment methods, theming, product metadata, and more.

Building the SDK Locally

  1. Clone the repository:
git clone https://github.com/Paella-Labs/crossmint-flutter-sdk.git
  1. Install dependencies:
cd crossmint-flutter-sdk
flutter pub get
  1. Run checks:
./scripts/check_pr_gate.sh

For dependency and publish validation:

./scripts/check_dependency_hygiene.sh

Generating API Reference Docs

The SDK includes a Python script that converts Dart /// doc comments into MDX files for the Mintlify docs site. It mirrors the approach used by the Swift SDK's docc-to-markdown.py.

Prerequisites

  • Python 3.10+ (no external dependencies — stdlib only)

Usage

From the repository root:

python3 scripts/dart-to-mdx.py \
  packages/crossmint_core/lib \
  packages/crossmint_auth/lib \
  packages/crossmint_wallets/lib \
  packages/crossmint_verifiable_credentials/lib \
  packages/crossmint_flutter/lib \
  --output /path/to/crossbit-main/apps/crossmint-mintlify-docs/src/sdk-reference/wallets/flutter \
  --documented-only

This scans the lib/ directories, extracts public symbols with doc comments, and writes one MDX file per class, enum, mixin, extension, typedef, and top-level function.

Options

Flag Description
--output, -o Required. Output directory for generated MDX files. Subdirectories (classes/, enums/, type-aliases/, etc.) are created automatically.
--documented-only Only emit top-level symbols that have /// doc comments. Recommended for published docs.

Output structure

<output-dir>/
  classes/           # One MDX per class, abstract class, sealed class
  enums/             # One MDX per enum
  type-aliases/      # One MDX per typedef
  mixins/            # One MDX per mixin
  extensions/        # One MDX per extension
  functions/         # One MDX per top-level function

After generating, commit the MDX files to crossbit-main and update the navigation in apps/crossmint-mintlify-docs/src/docs.json.

Running tests

The converter has a fixture-based test suite:

python3 -m unittest discover -s scripts/tests -v

Tests are also run automatically in CI via the PR Gate workflow.

Documentation

Known Limitations

  • iOS physical-device test automation is limited by Flutter #184254 — physical iOS validation requires manual QA.
  • Solana device signers are intentionally unsupported to match current Crossmint wallet behavior.
  • Passkey signers require explicit callbacks (Stage 1). Native platform passkey support (Stage 2) is pending.
  • Some Crossmint API families may require project entitlements.

Contact

To get in touch with the Crossmint team, visit our contact page or find us on X.

(back to top)

Libraries

crossmint_auth
Pure-Dart authentication primitives — auth state, storage contract, and shared auth models. Re-exports package:crossmint_auth/crossmint_auth.dart.
crossmint_client
Top-level client surface for the Crossmint SDK.
crossmint_core
Pure-Dart base primitives — config, transport, error types, chain validation, and shared models. Re-exports package:crossmint_core/crossmint_core.dart, plus the Flutter-side CrossmintClientConfig and SDK version constant.
crossmint_flutter
Default entry point for the Crossmint Flutter SDK.
crossmint_flutter_auth
Authentication surface for the Crossmint SDK.
crossmint_flutter_bridge
Hidden WebView signer bridge — public types.
crossmint_flutter_controllers
Controller surface for wiring Crossmint into a Flutter widget tree.
crossmint_flutter_integration
Aggregate integration barrel — the full headless SDK surface.
crossmint_flutter_runtime
Runtime configuration and storage adapters for the Crossmint SDK.
crossmint_flutter_ui
Opt-in default UI for the Crossmint Flutter SDK.
crossmint_flutter_wallets
Wallet and signer surface for the Crossmint SDK.
crossmint_models
Shared domain models used across the Crossmint SDK — wallets, signers, transactions, credentials, orders, tokens, and users.
crossmint_verifiable_credentials
Verifiable credentials surface — credential queries, locators, issuance, and verification helpers. Re-exports package:crossmint_verifiable_credentials/crossmint_verifiable_credentials.dart plus the core barrel.
crossmint_wallets
Pure-Dart wallet primitives — wallet models, signer abstractions, and API contracts. Re-exports package:crossmint_wallets/crossmint_wallets.dart.