Skip to main content

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.