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

> Create user wallets from your Flutter app in under 5 minutes

<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 the full example app in the SDK repo.
  </Card>
</CardGroup>

<Steps>
  <Step title="Install the SDK">
    Run the following command to install the SDK:

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

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

    ```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>
    ```

    <Note>
      If you only use email OTP login (no OAuth), you can skip this step.
    </Note>
  </Step>

  <Step title="Initialize the client and add providers">
    Create a `CrossmintWalletProvider` at the root of your app. This example uses [Crossmint Auth](/authentication/introduction)
    but you can use [any authentication provider of your choice](/wallets/guides/bring-your-own-auth).

    With the current setup, a wallet will be created automatically on login.

    See all supported chains [here](/introduction/supported-chains).

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

    ```bash theme={null}
    flutter run --dart-define=CROSSMINT_API_KEY=YOUR_CLIENT_API_KEY
    ```

    For detailed configuration options, see the [Flutter SDK Reference](/sdk-reference/wallets/flutter/providers#crossmintwalletprovider).
  </Step>

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

    ```dart home_screen.dart theme={null}
    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!'),
              ],
            ),
          ),
        );
      }
    }
    ```
  </Step>
</Steps>

## Launching in Production

For production, some changes are required:

1. Create a developer account on the <a href="https://www.crossmint.com/console" target="_blank">production console</a>
2. 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`, `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](/wallets/guides/bring-your-own-auth) with providers like Auth0, Firebase, or Supabase, rather than Crossmint Auth. Configure JWT authentication in the <a href="https://www.crossmint.com/console/projects/apiKeys" target="_blank">Crossmint Console</a> under API Keys > JWT Authentication.

## Learn More

<CardGroup cols={3}>
  <Card title="Check Balances" icon="money-bill-transfer" iconType="duotone" href="/wallets/guides/check-balances">
    Check the balance of a wallet.
  </Card>

  <Card title="Transfer Tokens" icon="coins" iconType="duotone" color="#1A5785" href="/wallets/guides/transfer-tokens">
    Send tokens between wallets.
  </Card>

  <Card title="Operational Signers" icon="key" iconType="duotone" color="#2156B9" href="/wallets/guides/signers/add-signers">
    Register operational signers on a wallet.
  </Card>
</CardGroup>

## Other Links

<CardGroup cols={2}>
  <Card title="API Reference" icon="terminal" color="#B56710" href="/api-reference/wallets/create-wallet">
    Deep dive into API reference docs.
  </Card>

  <Card title="Talk to an expert" icon="message" iconType="duotone" color="#ADD8E6" href="https://www.crossmint.com/contact/sales">
    Contact the Crossmint sales team for support.
  </Card>
</CardGroup>
