credential_manager 4.1.0 copy "credential_manager: ^4.1.0" to clipboard
credential_manager: ^4.1.0 copied to clipboard

Credential Manager plugin. Provides one-tap login functionality and stores credentials in the user's Google account on Android and in the Keychain on iOS.

example/lib/main.dart

import 'dart:async';
import 'package:credential_manager/credential_manager.dart';
import 'package:credential_manager_example/home_screen.dart';
import 'package:flutter/material.dart';
import 'dart:developer';

// Provide at build/run time, e.g.:
//   flutter run --dart-define=GOOGLE_CLIENT_ID=<your-client-id>.apps.googleusercontent.com
// CI supplies this from the GOOGLE_CLIENT_ID repository secret.
const String googleClientId = String.fromEnvironment('GOOGLE_CLIENT_ID');
// Web's rpId must match the origin the app is actually served from, so it's
// configurable at build/run time, e.g.:
//   flutter run --dart-define=RP_ID=localhost
//   flutter build web --dart-define=RP_ID=credential-manager-compose-example.netlify.app
// Defaults to "localhost" for local web development. Android/iOS keep a fixed
// domain since their rpId is tied to the hosted Digital Asset Links /
// apple-app-site-association files, not the client build.
const String _webRpId = String.fromEnvironment('RP_ID', defaultValue: 'localhost');
final String rpId = CredentialManagerPlatformManager.instance.isWeb ? _webRpId : "blogs-deeplink-example.vercel.app";
final CredentialManager credentialManager = CredentialManager();

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  if (credentialManager.isSupportedPlatform) {
    try {
      await credentialManager.init(
        preferImmediatelyAvailableCredentials: true,
        googleClientId: googleClientId.isNotEmpty ? googleClientId : null,
      );
    } on CredentialException catch (e) {
      log("Error initializing credential manager: ${e.message}");
    } catch (e) {
      log("Error initializing credential manager: $e");
    }
  }
  log("current platform: ${CredentialManagerPlatformManager.instance.isAndroid ? "android" : CredentialManagerPlatformManager.instance.isIOS ? "ios" : "web"}");
  log("is supported platform: ${credentialManager.isSupportedPlatform}");

  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Credential Manager Example",
      debugShowCheckedModeBanner: false,
      theme: buildAppTheme(Brightness.light),
      themeMode: ThemeMode.light,
      home: const LoginScreen(),
    );
  }
}

/// Shared Material 3 theme for both light and dark mode, seeded from a single
/// brand color so every component (buttons, cards, inputs, chips, snack bars)
/// stays visually consistent across the app.
ThemeData buildAppTheme(Brightness brightness) {
  final colorScheme = ColorScheme.fromSeed(
    seedColor: const Color(0xFF3762F5),
    brightness: brightness,
  );

  return ThemeData(
    useMaterial3: true,
    colorScheme: colorScheme,
    visualDensity: VisualDensity.adaptivePlatformDensity,
    appBarTheme: AppBarTheme(
      centerTitle: true,
      backgroundColor: colorScheme.surface,
      foregroundColor: colorScheme.onSurface,
      surfaceTintColor: colorScheme.surfaceTint,
      elevation: 0,
    ),
    cardTheme: CardThemeData(
      elevation: 1,
      surfaceTintColor: colorScheme.surfaceTint,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
    ),
    chipTheme: ChipThemeData(
      backgroundColor: colorScheme.surfaceContainerHighest,
      labelStyle: TextStyle(color: colorScheme.onSurfaceVariant),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
      side: BorderSide.none,
    ),
    inputDecorationTheme: InputDecorationTheme(
      filled: true,
      fillColor: colorScheme.surfaceContainerHighest,
      border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
      focusedBorder: OutlineInputBorder(
        borderRadius: BorderRadius.circular(12),
        borderSide: BorderSide(color: colorScheme.primary, width: 1.5),
      ),
    ),
    filledButtonTheme: FilledButtonThemeData(
      style: FilledButton.styleFrom(
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
        padding: const EdgeInsets.symmetric(vertical: 14),
      ),
    ),
    outlinedButtonTheme: OutlinedButtonThemeData(
      style: OutlinedButton.styleFrom(
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
        padding: const EdgeInsets.symmetric(vertical: 14),
      ),
    ),
    snackBarTheme: SnackBarThemeData(
      behavior: SnackBarBehavior.floating,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
    ),
  );
}

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

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

class _LoginScreenState extends State<LoginScreen> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  bool isLoading = false;
  bool createPassKey = false;
  String? username;
  String? password;
  late CredentialLoginOptions passKeyLoginOption;
  bool isGoogleEnabled = false;

  // Google Sign-In flow, selectable via the dropdown shown for Android/Web:
  // - false: One Tap / passive (GetGoogleIdOption on Android, One Tap on Web)
  // - true: button/active flow (GetSignInWithGoogleOption on Android, a
  //   rendered Google button click on Web)
  bool useGoogleButtonFlow = false;

  @override
  void initState() {
    super.initState();
    passKeyLoginOption = CredentialLoginOptions(
      challenge: "HjBbH__fbLuzy95AGR31yEARA0EMtKlY0NrV5oy3NQw",
      rpId: rpId,
      userVerification: "required",
      //only for ios, true only when we want to show the passkey popup on keyboard otherwise false
      conditionalUI: CredentialManagerPlatformManager.instance.isIOS,
    );
    isGoogleEnabled = googleClientId.isNotEmpty;
  }

  Widget _buildAutofillGroup(Widget child) {
    if (enableInlineAutofill) {
      return AutofillGroup(child: child);
    }
    return child;
  }

  @override
  Widget build(BuildContext context) {
    final isGmsAvailable = credentialManager.isGmsAvailable;
    final isCompact = MediaQuery.sizeOf(context).width < 600;

    return Scaffold(
      backgroundColor: Theme.of(context).colorScheme.surfaceContainerLowest,
      appBar: AppBar(
        title: const Text("Credential Manager"),
        centerTitle: true,
      ),
      body: !isGmsAvailable
          ? const Center(child: Text("Google Play Services is not available"))
          : Stack(
              children: [
                AbsorbPointer(
                  absorbing: isLoading,
                  child: Opacity(
                    opacity: isLoading ? 0.5 : 1,
                    child: SingleChildScrollView(
                      padding: EdgeInsets.symmetric(horizontal: isCompact ? 16 : 24, vertical: 24),
                      child: Center(
                        child: ConstrainedBox(
                          constraints: const BoxConstraints(maxWidth: 460),
                          child: _buildAutofillGroup(
                            Card(
                              elevation: isCompact ? 0 : 1,
                              color: isCompact ? Colors.transparent : Theme.of(context).colorScheme.surface,
                              surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
                              margin: EdgeInsets.zero,
                              child: Padding(
                                padding: EdgeInsets.all(isCompact ? 0 : 32),
                                child: Form(
                                  key: _formKey,
                                  child: Column(
                                    crossAxisAlignment: CrossAxisAlignment.stretch,
                                    children: [
                                      _buildHeader(),
                                      const SizedBox(height: 32),
                                      _buildInputField(
                                        "Username",
                                        (value) => username = value,
                                        icon: Icons.person_outline,
                                      ),
                                      if (createPassKey) ...[
                                        const SizedBox(height: 16),
                                        _buildInputField(
                                          "Password",
                                          (value) => password = value,
                                          isPassword: true,
                                          icon: Icons.lock_outline,
                                        ),
                                      ],
                                      const SizedBox(height: 24),
                                      _buildSectionTitle("Registration"),
                                      const SizedBox(height: 12),
                                      _buildActionButton(
                                        "Register with Password",
                                        onRegister,
                                        icon: Icons.password,
                                        isPrimary: true,
                                      ),
                                      const SizedBox(height: 12),
                                      _buildActionButton(
                                        "Register with Passkey",
                                        onRegisterWithPassKey,
                                        icon: Icons.key,
                                      ),
                                      if (isGoogleSignInSupported) ...[
                                        const SizedBox(height: 12),
                                        _buildGoogleFlowDropdown(),
                                        const SizedBox(height: 12),
                                        _buildActionButton(
                                          "Register with Google",
                                          onGoogleSignIn,
                                          icon: Icons.g_mobiledata,
                                        ),
                                      ],
                                      const SizedBox(height: 24),
                                      _buildSectionTitle("Login"),
                                      const SizedBox(height: 12),
                                      _buildActionButton(
                                        CredentialManagerPlatformManager.instance.isAndroid
                                            ? "Login (All Methods)"
                                            : isGoogleSignInSupported
                                                ? "Login (Passkey + Google)"
                                                : "Login with Passkey",
                                        onLogin,
                                        icon: Icons.login,
                                        isPrimary: true,
                                      ),
                                    ],
                                  ),
                                ),
                              ),
                            ),
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
                if (isLoading)
                  Container(
                    color: Colors.black26,
                    child: const Center(
                      child: Card(
                        child: Padding(
                          padding: EdgeInsets.all(24),
                          child: CircularProgressIndicator.adaptive(),
                        ),
                      ),
                    ),
                  ),
              ],
            ),
    );
  }

  bool enableInlineAutofill =
      CredentialManagerPlatformManager.instance.isIOS || CredentialManagerPlatformManager.instance.isWeb;

  // Google Sign-In is implemented on Android (Credential Manager) and Web (FedCM).
  bool get isGoogleSignInSupported =>
      isGoogleEnabled &&
      (CredentialManagerPlatformManager.instance.isAndroid || CredentialManagerPlatformManager.instance.isWeb);

  Widget _buildHeader() {
    return Column(
      children: [
        Container(
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            color: Theme.of(context).colorScheme.primaryContainer,
            borderRadius: BorderRadius.circular(16),
          ),
          child: Icon(
            Icons.security,
            size: 48,
            color: Theme.of(context).colorScheme.onPrimaryContainer,
          ),
        ),
        const SizedBox(height: 16),
        Text(
          'Secure Authentication',
          style: Theme.of(context).textTheme.headlineSmall?.copyWith(
                fontWeight: FontWeight.bold,
              ),
        ),
        const SizedBox(height: 8),
        Text(
          'Sign in with password, passkey, or Google',
          style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                color: Theme.of(context).colorScheme.onSurfaceVariant,
              ),
          textAlign: TextAlign.center,
        ),
        const SizedBox(height: 12),
        _buildPlatformChip(),
      ],
    );
  }

  Widget _buildPlatformChip() {
    final platformManager = CredentialManagerPlatformManager.instance;
    final (icon, label) = switch ((platformManager.isAndroid, platformManager.isIOS, platformManager.isWeb)) {
      (true, _, _) => (Icons.android, 'Android'),
      (_, true, _) => (Icons.apple, 'iOS'),
      (_, _, true) => (Icons.public, 'Web'),
      _ => (Icons.devices_other, 'Unknown'),
    };

    return Chip(
      avatar: Icon(icon, size: 18, color: Theme.of(context).colorScheme.primary),
      label: Text('Running on $label'),
      visualDensity: VisualDensity.compact,
    );
  }

  /// Lets the user pick between Google's One Tap/passive flow and its
  /// button/active flow. Both platforms that support Google Sign-In
  /// (Android + Web) accept the same `useButtonFlow` bool, so one dropdown
  /// covers both - see `saveGoogleCredential(bool useButtonFlow, {String? nonce})`.
  Widget _buildGoogleFlowDropdown() {
    return DropdownButtonFormField<bool>(
      initialValue: useGoogleButtonFlow,
      decoration: const InputDecoration(
        labelText: "Google Sign-In flow",
        prefixIcon: Icon(Icons.tune),
      ),
      items: const [
        DropdownMenuItem(value: false, child: Text("One Tap (passive)")),
        DropdownMenuItem(value: true, child: Text("Button flow (active)")),
      ],
      onChanged: (value) => setState(() => useGoogleButtonFlow = value ?? false),
    );
  }

  Widget _buildSectionTitle(String title) {
    return Row(
      children: [
        Text(
          title,
          style: Theme.of(context).textTheme.titleSmall?.copyWith(
                color: Theme.of(context).colorScheme.primary,
                fontWeight: FontWeight.w600,
              ),
        ),
        const SizedBox(width: 8),
        Expanded(
          child: Divider(
            color: Theme.of(context).colorScheme.outlineVariant,
          ),
        ),
      ],
    );
  }

  Widget _buildInputField(
    String hint,
    Function(String) onChanged, {
    bool isPassword = false,
    IconData? icon,
  }) {
    return TextFormField(
      onChanged: onChanged,
      obscureText: isPassword,
      autofillHints:
          enableInlineAutofill ? (isPassword ? const [AutofillHints.password] : const [AutofillHints.username]) : [],
      keyboardType: isPassword ? TextInputType.visiblePassword : null,
      validator: (value) => value!.isEmpty ? "Please enter a $hint" : null,
      decoration: InputDecoration(
        labelText: hint,
        prefixIcon: icon != null ? Icon(icon) : null,
      ),
    );
  }

  Widget _buildActionButton(
    String label,
    VoidCallback onPressed, {
    IconData? icon,
    bool isPrimary = false,
  }) {
    if (isPrimary) {
      return FilledButton.icon(
        onPressed: onPressed,
        icon: icon != null ? Icon(icon) : const SizedBox.shrink(),
        label: Text(label),
      );
    }

    return OutlinedButton.icon(
      onPressed: onPressed,
      icon: icon != null ? Icon(icon) : const SizedBox.shrink(),
      label: Text(label),
    );
  }

  Future<void> onRegister() async {
    if (_formKey.currentState!.validate()) {
      if (!createPassKey) {
        setState(() => createPassKey = true);
      } else {
        if (enableInlineAutofill) {
          _navigateToHomeScreen(Credential.password,
              passwordCredential: PasswordCredential(
                username: username,
                password: password,
              ));
          return;
        }

        await _performAction(() async {
          await credentialManager.savePasswordCredentials(
            PasswordCredential(username: username, password: password),
          );

          _showSnackBar("Successfully saved credential");
          _navigateToHomeScreen(Credential.password,
              passwordCredential: PasswordCredential(
                username: username,
                password: password,
              ));
        });
      }
    }
  }

  Future<void> onRegisterWithPassKey() async {
    if (_formKey.currentState!.validate()) {
      await _performAction(() async {
        final credentialCreationOptions = {
          "challenge": "HjBbH__fbLuzy95AGR31yEARA0EMtKlY0NrV5oy3NQw",
          "rp": {"name": "CredMan App Test", "id": rpId},
          "user": {
            "id": EncryptData.getEncodedUserId(),
            "name": username,
            "displayName": username,
          },
          "excludeCredentials": [
            {"id": "ghi789", "type": "public-key"},
            {"id": "jkl012", "type": "public-key"}
          ],
        };

        if (!CredentialManagerPlatformManager.instance.isIOS) {
          credentialCreationOptions.addAll({
            "pubKeyCredParams": [
              {"type": "public-key", "alg": -7},
              {"type": "public-key", "alg": -257}
            ],
            "timeout": 1800000,
            "attestation": "none",
            "authenticatorSelection": {
              "authenticatorAttachment": "platform",
              "residentKey": "required",
              "userVerification": "required"
            }
          });
        }

        final res = await credentialManager.savePasskeyCredentials(
          request: CredentialCreationOptions.fromJson(credentialCreationOptions),
        );
        _showSnackBar("Successfully saved credential");
        _navigateToHomeScreen(Credential.passkey, publicKeyCredential: res);
      });
    }
  }

  Future<void> onGoogleSignIn() async {
    await _performAction(() async {
      // Passing a nonce is optional. If you omit it, the plugin generates a
      // securely-random one for you (see CredentialManagerPlatformManager
      // capabilities: GetGoogleIdOption/GetSignInWithGoogleOption on Android,
      // FedCM IdentityProvider on web). Supply your own when you need to tie
      // the sign-in request to a value your backend already issued.
      final credential = await credentialManager.saveGoogleCredential(
        useButtonFlow: useGoogleButtonFlow,
        nonce: EncryptData.getEncodedChallenge(),
      );
      _showSnackBar("Successfully retrieved credential");
      _navigateToHomeScreen(Credential.google, googleIdTokenCredential: credential);
    });
  }

  Future<void> onLogin() async {
    await _performAction(() async {
      Credentials credential = await credentialManager.getCredentials(
        passKeyOption: passKeyLoginOption,
        fetchOptions: FetchOptionsAndroid(
          passKey: true,
          passwordCredential: true,
          googleCredential: isGoogleSignInSupported,
        ),
      );
      _showLoginSuccessDialog(credential);
    });
  }

  Future<void> _performAction(Future<void> Function() action) async {
    setState(() => isLoading = true);
    try {
      await action();
    } on CredentialException catch (e) {
      log("Error performing action: ${e.message}, code: ${e.code} , details: ${e.details}");
      _showSnackBar(e.message.toString());
    } finally {
      if (mounted) setState(() => isLoading = false);
    }
  }

  void _showSnackBar(String message) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
  }

  void _showLoginSuccessDialog(Credentials credential) {
    bool isPasswordBasedCredentials = credential.passwordCredential != null;
    bool isPublicKeyBasedCredentials = credential.publicKeyCredential != null;
    _showSnackBar("Successfully retrieved credential");

    _navigateToHomeScreen(
      isPasswordBasedCredentials
          ? Credential.password
          : isPublicKeyBasedCredentials
              ? Credential.passkey
              : Credential.google,
      googleIdTokenCredential: credential.googleIdTokenCredential,
      passwordCredential: credential.passwordCredential,
      publicKeyCredential: credential.publicKeyCredential,
    );
  }

  void _navigateToHomeScreen(Credential credentialType,
      {GoogleIdTokenCredential? googleIdTokenCredential,
      PasswordCredential? passwordCredential,
      PublicKeyCredential? publicKeyCredential}) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => HomeScreen(
          credentialType: credentialType,
          passwordCredential: passwordCredential,
          publicKeyCredential: publicKeyCredential,
          googleIdTokenCredential: googleIdTokenCredential,
        ),
      ),
    );
  }
}
37
likes
160
points
3.4k
downloads

Documentation

API reference

Publisher

verified publishersmkwinner.live

Weekly Downloads

Credential Manager plugin. Provides one-tap login functionality and stores credentials in the user's Google account on Android and in the Keychain on iOS.

Homepage
Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

credential_manager_android, credential_manager_ios, credential_manager_platform_interface, credential_manager_web, flutter

More

Packages that depend on credential_manager

Packages that implement credential_manager