> ## 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.

# Flutter

> Authenticate users in your Flutter app in under 5 minutes

<Snippet file="auth-staging-note.mdx" />

<Note>
  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`.
</Note>

<CardGroup cols={2}>
  <Snippet file="before-you-start.mdx" />

  <Card title="Flutter Wallets Example" icon="github" iconType="duotone" href="https://github.com/Paella-Labs/crossmint-flutter-sdk">
    See a full working example with auth and wallets.
  </Card>
</CardGroup>

<Steps>
  <Step title="Install the SDK">
    Install the Crossmint Flutter SDK:

    <Snippet file="client-sdk-flutter-installation-cmd.mdx" />

    The SDK bundles the following dependencies:

    | Package                  | Purpose                                                        |
    | ------------------------ | -------------------------------------------------------------- |
    | `flutter_secure_storage` | Encrypted token storage (Keychain on iOS, Keystore on Android) |
    | `url_launcher`           | Opens the OAuth consent screen in a system browser             |
    | `app_links`              | Listens for OAuth deep-link redirects back to your app         |
  </Step>

  <Step title="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`:

    ```xml android/app/src/main/AndroidManifest.xml theme={null}
    <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`:

    ```xml ios/Runner/Info.plist theme={null}
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleURLSchemes</key>
            <array>
                <string>myapp</string>
            </array>
        </dict>
    </array>
    ```

    Pass the same scheme to `CrossmintClientConfig.appScheme`.
  </Step>

  <Step title="Initialize the client">
    Initialize `CrossmintClient` and restore any persisted session on app launch.

    ```dart lib/crossmint.dart theme={null}
    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',
      ),
    );
    ```

    ```dart main.dart theme={null}
    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:

    ```bash theme={null}
    flutter run --dart-define=CROSSMINT_API_KEY=YOUR_CLIENT_API_KEY
    ```
  </Step>

  <Step title="Build the login screen">
    Use the `CrossmintAuthClient` to implement email OTP and OAuth login flows.

    ### Email OTP Login

    ```dart login_screen.dart theme={null}
    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)

    ```dart oauth_button.dart theme={null}
    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),
            ),
          ),
        );
      }
    }
    ```

    <Accordion title="How OAuth Works on Android vs iOS">
      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.
    </Accordion>
  </Step>

  <Step title="Add logout and display user info">
    ```dart home_screen.dart theme={null}
    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}'),
                  ],
                ),
              );
            },
          ),
        );
      }
    }
    ```
  </Step>
</Steps>

## Auth State Values

The `CrossmintAuthState` provides the following status information:

| Status         | Description                                                    |
| -------------- | -------------------------------------------------------------- |
| `initializing` | The SDK is loading stored tokens and checking session validity |
| `loggedOut`    | No active session. Show your login screen                      |
| `inProgress`   | An OAuth or OTP flow is currently in progress                  |
| `loggedIn`     | The 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

| Feature              | React Native                               | Flutter                                                             |
| -------------------- | ------------------------------------------ | ------------------------------------------------------------------- |
| **Login UI**         | Headless — build your own UI               | Headless — build your own UI (optional pre-built widgets available) |
| **State management** | React hooks (`useCrossmintAuth`)           | `ChangeNotifier` / `ValueListenable`                                |
| **OAuth flow**       | System browser via `expo-web-browser`      | System browser via `url_launcher`                                   |
| **OAuth callback**   | `Linking.useURL()` + `createAuthSession()` | `CrossmintAuthCallbackRouter` (automatic)                           |
| **Token storage**    | `expo-secure-store` (encrypted)            | `flutter_secure_storage` (encrypted)                                |
| **Package**          | `@crossmint/client-sdk-react-native-ui`    | `crossmint_flutter`                                                 |
| **Deep link setup**  | `scheme` in `app.json`                     | `AndroidManifest.xml` + `Info.plist`                                |
| **OTP prompts**      | Manual via `useWalletOtpSigner` hook       | Automatic via `CrossmintOtpSignerListener` widget                   |

## Moving to Production

<Warning>
  **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.
</Warning>

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](/wallets/guides/bring-your-own-auth) to integrate it with Crossmint via JWT.
2. Create a developer account on the <a href="https://www.crossmint.com/console" target="_blank">production console</a>.
3. Create a production client API key on the <a href="https://www.crossmint.com/console/projects/apiKeys" target="_blank">API Keys</a> page with the API scopes `users.create`, `users.read`, `wallets.read`.
4. [Configure JWT authentication](/introduction/platform/api-keys/jwt-authentication) for your auth provider in the Crossmint Console.

## Next Steps

<CardGroup cols={3}>
  <Card title="Bring Your Own Auth" icon="key" href="/wallets/guides/bring-your-own-auth">
    Recommended for production — integrate your own auth provider via JWT
  </Card>

  <Card title="Webhooks" icon="bell" href="/authentication/webhooks">
    Get notified when a user signs up or updates their profile
  </Card>

  <Card title="Create Smart Wallets" icon="wallet" href="/wallets/quickstarts/flutter">
    Provision wallets automatically when users sign up
  </Card>
</CardGroup>
