Skip to main content
Crossmint Auth is designed for staging and moving fast. For production applications, Crossmint strongly recommends connecting your own authentication provider for full control over user management. See the Bring Your Own Auth guide.
Crossmint Auth on Flutter is headless — there is no pre-built login modal. You build your own UI and call the SDK directly. This guide walks through both email OTP and OAuth login flows. Optional pre-built widgets are available via crossmint_flutter_ui.dart.

Before you start

Set up your project and get an API key.

Flutter Wallets Example

See a full working example with auth and wallets.
1

Install the SDK

Install the Crossmint Flutter SDK:
flutter pub add crossmint_flutter
The SDK bundles the following dependencies:
PackagePurpose
flutter_secure_storageEncrypted token storage (Keychain on iOS, Keystore on Android)
url_launcherOpens the OAuth consent screen in a system browser
app_linksListens for OAuth deep-link redirects back to your app
2

Configure your deep link scheme

OAuth redirects require a deep link scheme so the browser can return to your app after login.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>
Pass the same scheme to CrossmintClientConfig.appScheme.
3

Initialize the client

Initialize CrossmintClient and restore any persisted session on app launch.
lib/crossmint.dart
import 'package:crossmint_flutter/crossmint_client.dart';
import 'package:crossmint_flutter/crossmint_flutter_auth.dart';

/// Shared Crossmint client instance — import this file from your screens.
final client = CrossmintClient(
  config: CrossmintClientConfig(
    apiKey: const String.fromEnvironment('CROSSMINT_API_KEY'),
    appScheme: 'myapp',
  ),
);
main.dart
import 'package:flutter/material.dart';
import 'package:crossmint_flutter/crossmint_flutter_auth.dart';
import 'crossmint.dart'; // shared client instance

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await client.initialize();
  await client.auth.restoreSession();

  // Start the OAuth callback router
  final authRouter = CrossmintAuthCallbackRouter(auth: client.auth);
  await authRouter.start();

  runApp(const MyApp());
}
Pass the API key via --dart-define when running the app:
flutter run --dart-define=CROSSMINT_API_KEY=YOUR_CLIENT_API_KEY
4

Build the login screen

Use the CrossmintAuthClient to implement email OTP and OAuth login flows.

Email OTP Login

login_screen.dart
import 'package:flutter/material.dart';
import 'package:crossmint_flutter/crossmint_flutter_auth.dart';
import 'crossmint.dart'; // shared client instance

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 address')),
      );
      return;
    }

    setState(() => _isPending = true);
    try {
      final challenge = await client.auth.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 {
      await client.auth.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) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            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)),
                ),
              ),
            ],
          ],
        ),
      ),
    );
  }

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

OAuth Login (Google, Twitter)

oauth_button.dart
import 'package:flutter/material.dart';
import 'package:crossmint_flutter/crossmint_flutter_auth.dart';
import 'crossmint.dart'; // shared client instance

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

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: double.infinity,
      child: ElevatedButton(
        onPressed: () => client.auth.loginWithOAuth(
          CrossmintOAuthProvider.google,
        ),
        style: ElevatedButton.styleFrom(
          backgroundColor: const Color(0xFF4285F4),
        ),
        child: const Text(
          'Sign in with Google',
          style: TextStyle(color: Colors.white),
        ),
      ),
    );
  }
}
The SDK handles platform differences automatically via CrossmintAuthCallbackRouter:
  • iOS: Uses url_launcher to open an in-app authentication session. The redirect URL is captured by the auth callback router via app_links.
  • Android: Opens the system browser. The OAuth provider redirects back to your app via the deep link scheme configured in AndroidManifest.xml.
The CrossmintAuthCallbackRouter listens for incoming deep links on both platforms and automatically completes the OAuth flow — no manual URL handling required.
5

Add logout and display user info

home_screen.dart
import 'package:flutter/material.dart';
import 'package:crossmint_flutter/crossmint_flutter_auth.dart';
import 'crossmint.dart'; // shared client instance

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ValueListenableBuilder<CrossmintAuthState>(
        valueListenable: client.auth.stateListenable,
        builder: (context, state, _) {
          final user = state.user;
          if (user == null) return const LoginScreen();

          return 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: () => client.auth.logout(),
                      child: const Text('Logout',
                          style: TextStyle(color: Colors.red)),
                    ),
                  ],
                ),
                const SizedBox(height: 20),
                Text('Email: ${user.email}'),
                Text('User ID: ${user.id}'),
              ],
            ),
          );
        },
      ),
    );
  }
}

Auth State Values

The CrossmintAuthState provides the following status information:
StatusDescription
initializingThe SDK is loading stored tokens and checking session validity
loggedOutNo active session. Show your login screen
inProgressAn OAuth or OTP flow is currently in progress
loggedInThe user is authenticated. user and jwt are available

Token Storage

By default, the SDK stores authentication tokens using flutter_secure_storage, which encrypts data at rest using the platform keychain (iOS) or keystore (Android). Tokens persist across app restarts and are protected by the device’s hardware-backed security. You can provide a custom storage implementation via CrossmintClientConfig.authStorage if you need a different storage backend. The SDK also provides InMemoryCrossmintAuthStorage for testing.

Differences from the React Native SDK

FeatureReact NativeFlutter
Login UIHeadless — build your own UIHeadless — build your own UI (optional pre-built widgets available)
State managementReact hooks (useCrossmintAuth)ChangeNotifier / ValueListenable
OAuth flowSystem browser via expo-web-browserSystem browser via url_launcher
OAuth callbackLinking.useURL() + createAuthSession()CrossmintAuthCallbackRouter (automatic)
Token storageexpo-secure-store (encrypted)flutter_secure_storage (encrypted)
Package@crossmint/client-sdk-react-native-uicrossmint_flutter
Deep link setupscheme in app.jsonAndroidManifest.xml + Info.plist
OTP promptsManual via useWalletOtpSigner hookAutomatic via CrossmintOtpSignerListener widget

Moving to Production

Crossmint Auth is designed for staging and getting started quickly. For production applications, Crossmint strongly recommends migrating to your own authentication provider for full control over user management.
When you are ready to go to production, Crossmint recommends:
  1. Set up your own auth provider (Auth0, Firebase, Supabase, Stytch, etc.) and follow the Bring Your Own Auth guide to integrate it with Crossmint via JWT.
  2. Create a developer account on the production console.
  3. Create a production client API key on the API Keys page with the API scopes users.create, users.read, wallets.read.
  4. Configure JWT authentication for your auth provider in the Crossmint Console.

Next Steps

Bring Your Own Auth

Recommended for production — integrate your own auth provider via JWT

Webhooks

Get notified when a user signs up or updates their profile

Create Smart Wallets

Provision wallets automatically when users sign up