Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.crossmint.com/llms.txt

Use this file to discover all available pages before exploring further.

Before you start

Set up your project and get an API key.

Flutter Wallets Example

See the full example app in the SDK repo.
1

Install the SDK

Run the following command to install the SDK:
flutter pub add crossmint_flutter
2

Configure deep link scheme for OAuth

If you plan to use OAuth login (Google, Twitter), you need a deep link scheme so the browser can return to your app after authentication.Android — add to your AndroidManifest.xml:
android/app/src/main/AndroidManifest.xml
<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="myapp" />
</intent-filter>
iOS — add to your Info.plist:
ios/Runner/Info.plist
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string>
        </array>
    </dict>
</array>
If you only use email OTP login (no OAuth), you can skip this step.
3

Initialize the client and add providers

Create a CrossmintWalletProvider at the root of your app. This example uses Crossmint Auth but you can use any authentication provider of your choice.With the current setup, a wallet will be created automatically on login.See all supported chains here.
main.dart
import 'package:flutter/material.dart';
import 'package:crossmint_flutter/crossmint_flutter_ui.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Crossmint Flutter Wallets',
      home: CrossmintWalletProvider(
        config: CrossmintWalletProviderConfig(
          clientConfig: CrossmintClientConfig(
            apiKey: const String.fromEnvironment('CROSSMINT_API_KEY'),
            appScheme: 'myapp',
          ),
          walletControllerConfig: CrossmintWalletControllerConfig(
            createOnLogin: CrossmintCreateOnLoginConfig(
              chain: 'base-sepolia',
              recovery: const CrossmintEmailSignerConfig(),
            ),
            showOtpSignerPrompt: true,
          ),
          otpPromptBuilder: crossmintDefaultOtpPromptBuilder,
        ),
        child: const HomeScreen(),
      ),
    );
  }
}
Pass the API key via --dart-define when running the app:
flutter run --dart-define=CROSSMINT_API_KEY=YOUR_CLIENT_API_KEY
For detailed configuration options, see the Flutter SDK Reference.
4

Allow users to login, logout, and access their wallet

Crossmint Auth in Flutter is headless — you build your own login UI and call the SDK directly. Use CrossmintWalletContext to access the auth client and wallet controller.
home_screen.dart
import 'package:flutter/material.dart';
import 'package:crossmint_flutter/crossmint_flutter_ui.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return CrossmintWalletGate(
      unauthenticatedBuilder: (context, data) {
        return const LoginScreen();
      },
      initializingBuilder: (context, data) {
        return const Scaffold(
          body: Center(child: CircularProgressIndicator()),
        );
      },
      readyBuilder: (context, data) {
        // The gate fires readyBuilder once authenticated, but
        // the wallet may still be loading. `hasWallet` is the
        // convenience predicate — equivalent to `currentWallet != null`.
        if (!data.state.hasWallet) {
          return const Scaffold(
            body: Center(child: CircularProgressIndicator()),
          );
        }
        return WalletScreen(wallet: data.state.currentWallet!);
      },
    );
  }
}

class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _emailController = TextEditingController();
  final _otpController = TextEditingController();
  String? _emailId;
  bool _otpSent = false;
  bool _isPending = false;

  Future<void> _sendOtp() async {
    final email = _emailController.text.trim();
    if (email.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Enter a valid email')),
      );
      return;
    }

    setState(() => _isPending = true);
    try {
      final walletContext = CrossmintWalletContext.of(context);
      final challenge = await walletContext.requireAuth.sendEmailOtp(email);
      setState(() {
        _emailId = challenge.id;
        _otpSent = true;
      });
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to send OTP: $e')),
      );
    } finally {
      setState(() => _isPending = false);
    }
  }

  Future<void> _verifyOtp() async {
    final otp = _otpController.text.trim();
    if (otp.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Enter the OTP code')),
      );
      return;
    }

    setState(() => _isPending = true);
    try {
      final walletContext = CrossmintWalletContext.of(context);
      await walletContext.requireAuth.confirmEmailOtp(
        email: _emailController.text.trim(),
        emailId: _emailId!,
        token: otp,
      );
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Invalid OTP: $e')),
      );
    } finally {
      setState(() => _isPending = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    final walletContext = CrossmintWalletContext.of(context);

    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Login', style: TextStyle(fontSize: 24)),
            const SizedBox(height: 20),
            TextField(
              controller: _emailController,
              decoration: const InputDecoration(
                hintText: 'Enter your email',
                border: OutlineInputBorder(),
              ),
              enabled: !_otpSent,
            ),
            const SizedBox(height: 10),
            if (!_otpSent) ...[
              SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  onPressed: _isPending ? null : _sendOtp,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFF05B959),
                  ),
                  child: _isPending
                      ? const SizedBox(
                          height: 20,
                          width: 20,
                          child: CircularProgressIndicator(
                            color: Colors.white,
                            strokeWidth: 2,
                          ),
                        )
                      : const Text('Send OTP',
                          style: TextStyle(color: Colors.white)),
                ),
              ),
            ] else ...[
              TextField(
                controller: _otpController,
                decoration: const InputDecoration(
                  hintText: 'Enter OTP code',
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 10),
              SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  onPressed: _isPending ? null : _verifyOtp,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFF05B959),
                  ),
                  child: _isPending
                      ? const SizedBox(
                          height: 20,
                          width: 20,
                          child: CircularProgressIndicator(
                            color: Colors.white,
                            strokeWidth: 2,
                          ),
                        )
                      : const Text('Verify OTP',
                          style: TextStyle(color: Colors.white)),
                ),
              ),
              const SizedBox(height: 10),
              SizedBox(
                width: double.infinity,
                child: OutlinedButton(
                  onPressed: () => setState(() => _otpSent = false),
                  child: const Text('Back'),
                ),
              ),
            ],
            const SizedBox(height: 20),
            const Text('OR'),
            const SizedBox(height: 10),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: () => walletContext.requireAuth
                    .loginWithOAuth(CrossmintOAuthProvider.google),
                style: ElevatedButton.styleFrom(
                  backgroundColor: const Color(0xFF4285F4),
                ),
                child: const Text('Sign in with Google',
                    style: TextStyle(color: Colors.white)),
              ),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _emailController.dispose();
    _otpController.dispose();
    super.dispose();
  }
}

class WalletScreen extends StatelessWidget {
  final CrossmintRuntimeWalletBase wallet;

  const WalletScreen({super.key, required this.wallet});

  @override
  Widget build(BuildContext context) {
    final walletContext = CrossmintWalletContext.of(context);

    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text('Welcome!',
                    style: TextStyle(fontSize: 20)),
                TextButton(
                  onPressed: () => walletContext.requireAuth.logout(),
                  child: const Text('Logout',
                      style: TextStyle(color: Colors.red)),
                ),
              ],
            ),
            const SizedBox(height: 20),
            const Text('Wallet Info:',
                style: TextStyle(fontSize: 16)),
            Text('Address: ${wallet.address}'),
            Text('Chain: ${wallet.chain}'),
            const SizedBox(height: 20),
            const Text('You are now logged in and can access your wallet!'),
          ],
        ),
      ),
    );
  }
}

Launching in Production

For production, some changes are required:
  1. Create a developer account on the production console
  2. Create a production client API key on the API Keys page with the API scopes users.create, users.read, wallets.read, wallets.create, wallets:transactions.create, wallets:transactions.sign, wallets:balance.read, wallets.fund
  3. Replace your test API key with the production key
  4. Use your own authentication provider: For production applications, Crossmint recommends using third-party authentication with providers like Auth0, Firebase, or Supabase, rather than Crossmint Auth. Configure JWT authentication in the Crossmint Console under API Keys > JWT Authentication.

Learn More

Check Balances

Check the balance of a wallet.

Transfer Tokens

Send tokens between wallets.

Operational Signers

Register operational signers on a wallet.

API Reference

Deep dive into API reference docs.

Talk to an expert

Contact the Crossmint sales team for support.