hng_firebase_auth 1.0.1 copy "hng_firebase_auth: ^1.0.1" to clipboard
hng_firebase_auth: ^1.0.1 copied to clipboard

A simple and modular Firebase authentication wrapper for Flutter, supporting email/password, Google, and Apple sign-in.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:hng_firebase_auth/hng_firebase_auth.dart';

import 'firebase_options.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => AuthProvider(
        config: const AuthConfig(
          providers: {
            'email': true,
            'google': true,
            'apple': true,
          },
        ),
      ),
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        title: 'HNG Firebase Auth Example',
        theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(
            seedColor: const Color(0xFF6366F1),
            brightness: Brightness.light,
          ),
          typography: Typography.material2021(),
        ),
        home: const MainDemoScreen(),
      ),
    );
  }
}

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

  @override
  State<MainDemoScreen> createState() => _MainDemoScreenState();
}

class _MainDemoScreenState extends State<MainDemoScreen>
    with SingleTickerProviderStateMixin {
  late final TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 2, vsync: this);
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) => [
          SliverAppBar(
            expandedHeight: 140,
            floating: true,
            snap: true,
            elevation: innerBoxIsScrolled ? 2 : 0,
            flexibleSpace: FlexibleSpaceBar(
              background: Container(
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topLeft,
                    end: Alignment.bottomRight,
                    colors: [
                      Theme.of(context).colorScheme.primary,
                      Theme.of(context)
                          .colorScheme
                          .primary
                          .withValues(alpha: 0.8),
                    ],
                  ),
                ),
              ),
            ),
            bottom: TabBar(
              controller: _tabController,
              tabs: const [
                Tab(text: 'Pre-built UI', icon: Icon(Icons.widgets)),
                Tab(text: 'Headless', icon: Icon(Icons.code)),
              ],
            ),
          ),
        ],
        body: TabBarView(
          controller: _tabController,
          children: const [
            PrebuiltUiExample(),
            HeadlessUiExample(),
          ],
        ),
      ),
    );
  }
}

/// Pre-built UI Example
///
/// This example shows how to use the ready-made AuthWidget from the SDK.
/// Simply drop it in and handle the success/error callbacks.
class PrebuiltUiExample extends StatelessWidget {
  const PrebuiltUiExample({super.key});

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
          child: ConstrainedBox(
            constraints: const BoxConstraints(maxWidth: 420),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'Ready-to-use',
                  style: Theme.of(context).textTheme.headlineMedium?.copyWith(
                        fontWeight: FontWeight.w700,
                        letterSpacing: -0.5,
                      ),
                ),
                const SizedBox(height: 8),
                Text(
                  'Just drop in AuthWidget and let it handle the rest',
                  style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                        color: Colors.grey[600],
                      ),
                ),
                const SizedBox(height: 32),
                Container(
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(16),
                    boxShadow: [
                      BoxShadow(
                        color: Colors.black.withValues(alpha: 0.08),
                        blurRadius: 20,
                        offset: const Offset(0, 8),
                      ),
                    ],
                  ),
                  child: AuthWidget(
                    onSuccess: () {
                      if (context.mounted) {
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(
                            content: const Text('Welcome! 🎉'),
                            backgroundColor: Colors.green[600],
                            duration: const Duration(seconds: 2),
                            behavior: SnackBarBehavior.floating,
                            margin: const EdgeInsets.all(16),
                          ),
                        );
                      }
                    },
                    onError: (error) {
                      if (context.mounted) {
                        // Handle specific exception types if needed
                        String message = error.message;
                        IconData icon = Icons.error;

                        if (error is NetworkException) {
                          icon = Icons.wifi_off;
                        } else if (error is InvalidCredentialsException) {
                          icon = Icons.password;
                        } else if (error is SignInCancelledException) {
                          // User cancelled - don't show error
                          return;
                        }

                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(
                            content: Row(
                              children: [
                                Icon(icon, color: Colors.white, size: 20),
                                const SizedBox(width: 12),
                                Expanded(child: Text(message)),
                              ],
                            ),
                            backgroundColor: Colors.red[600],
                            behavior: SnackBarBehavior.floating,
                            margin: const EdgeInsets.all(16),
                          ),
                        );
                      }
                    },
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

/// Headless Mode Example
///
/// This example shows how to use the SDK's AuthProvider directly
/// for complete control over your authentication flow.
class HeadlessUiExample extends StatefulWidget {
  const HeadlessUiExample({super.key});

  @override
  State<HeadlessUiExample> createState() => _HeadlessUiExampleState();
}

class _HeadlessUiExampleState extends State<HeadlessUiExample> {
  final _emailCtrl = TextEditingController();
  final _passCtrl = TextEditingController();
  bool _showPassword = false;

  late final Stream<AuthUser?> _userStream;

  Stream<AuthUser?> createAuthUserStream(AuthProvider provider) {
    late final StreamController<AuthUser?> controller;

    void listener() {
      if (!controller.isClosed) {
        controller.add(provider.user);
      }
    }

    controller = StreamController<AuthUser?>.broadcast(
      onListen: () {
        controller.add(provider.user);
        provider.addListener(listener);
      },
      onCancel: () {
        try {
          provider.removeListener(listener);
        } catch (_) {}
        controller.close();
      },
    );

    return controller.stream;
  }

  @override
  void initState() {
    super.initState();
    final provider = Provider.of<AuthProvider>(context, listen: false);
    _userStream = createAuthUserStream(provider);
  }

  @override
  void dispose() {
    _emailCtrl.dispose();
    _passCtrl.dispose();
    super.dispose();
  }

  /// Sign in with email - demonstrates typed exception handling
  Future<void> _signInWithEmail() async {
    final provider = context.read<AuthProvider>();
    try {
      await provider.signInWithEmail(_emailCtrl.text.trim(), _passCtrl.text);
    } on InvalidCredentialsException catch (e) {
      _showErrorSnackBar(e.message, icon: Icons.password);
    } on UserNotFoundException catch (e) {
      _showErrorSnackBar(e.message, icon: Icons.person_off);
    } on InvalidEmailException catch (e) {
      _showErrorSnackBar(e.message, icon: Icons.email);
    } on NetworkException catch (e) {
      _showErrorSnackBar(e.message, icon: Icons.wifi_off);
    } on TooManyRequestsException catch (e) {
      _showErrorSnackBar(e.message, icon: Icons.timer_off);
    } on AuthException catch (e) {
      // Catch-all for any other auth exceptions
      _showErrorSnackBar(e.message);
    }
  }

  /// Sign in with Google - demonstrates handling cancellation
  Future<void> _signInWithGoogle() async {
    final provider = context.read<AuthProvider>();
    try {
      await provider.signInWithGoogle();
    } on SignInCancelledException {
      // User cancelled - silently return without showing an error
      return;
    } on NetworkException catch (e) {
      _showErrorSnackBar(e.message, icon: Icons.wifi_off);
    } on AuthException catch (e) {
      _showErrorSnackBar(e.message);
    }
  }

  /// Sign out - handles any sign-out errors
  Future<void> _signOut() async {
    final provider = context.read<AuthProvider>();
    try {
      await provider.signOut();
    } on AuthException catch (e) {
      _showErrorSnackBar(e.message);
    }
  }

  /// Helper method to show error snackbar with optional icon
  void _showErrorSnackBar(String message, {IconData? icon}) {
    if (!context.mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Row(
          children: [
            if (icon != null) ...[
              Icon(icon, color: Colors.white, size: 20),
              const SizedBox(width: 12),
            ],
            Expanded(child: Text(message)),
          ],
        ),
        backgroundColor: Colors.red[600],
        behavior: SnackBarBehavior.floating,
        margin: const EdgeInsets.all(16),
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
        child: ConstrainedBox(
          constraints: const BoxConstraints(maxWidth: 500),
          child: Column(
            children: [
              StreamBuilder<AuthUser?>(
                stream: _userStream,
                builder: (context, snapshot) {
                  final user = snapshot.data;

                  if (user == null) {
                    return Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          'Sign in to continue',
                          style: Theme.of(context)
                              .textTheme
                              .headlineMedium
                              ?.copyWith(
                                fontWeight: FontWeight.w700,
                                letterSpacing: -0.5,
                              ),
                        ),
                        const SizedBox(height: 8),
                        Text(
                          'Build custom auth flows with full control',
                          style:
                              Theme.of(context).textTheme.bodyMedium?.copyWith(
                                    color: Colors.grey[600],
                                  ),
                        ),
                        const SizedBox(height: 32),
                        TextField(
                          controller: _emailCtrl,
                          decoration: InputDecoration(
                            hintText: 'you@example.com',
                            prefixIcon: const Icon(Icons.mail_outline),
                            border: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(12),
                              borderSide: const BorderSide(width: 1.5),
                            ),
                            enabledBorder: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(12),
                              borderSide: BorderSide(
                                width: 1.5,
                                color: Colors.grey[300]!,
                              ),
                            ),
                          ),
                          keyboardType: TextInputType.emailAddress,
                        ),
                        const SizedBox(height: 16),
                        TextField(
                          controller: _passCtrl,
                          obscureText: !_showPassword,
                          decoration: InputDecoration(
                            hintText: 'Password',
                            prefixIcon: const Icon(Icons.lock_outline),
                            suffixIcon: IconButton(
                              icon: Icon(
                                _showPassword
                                    ? Icons.visibility
                                    : Icons.visibility_off,
                              ),
                              onPressed: () {
                                setState(() {
                                  _showPassword = !_showPassword;
                                });
                              },
                            ),
                            border: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(12),
                              borderSide: const BorderSide(width: 1.5),
                            ),
                            enabledBorder: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(12),
                              borderSide: BorderSide(
                                width: 1.5,
                                color: Colors.grey[300]!,
                              ),
                            ),
                          ),
                        ),
                        const SizedBox(height: 24),
                        Consumer<AuthProvider>(
                          builder: (context, provider, _) {
                            return Column(
                              children: [
                                SizedBox(
                                  width: double.infinity,
                                  height: 48,
                                  child: ElevatedButton(
                                    onPressed: provider.isLoading
                                        ? null
                                        : _signInWithEmail,
                                    style: ElevatedButton.styleFrom(
                                      shape: RoundedRectangleBorder(
                                        borderRadius: BorderRadius.circular(12),
                                      ),
                                    ),
                                    child: provider.isLoading
                                        ? const SizedBox(
                                            height: 20,
                                            width: 20,
                                            child: CircularProgressIndicator(
                                              strokeWidth: 2.5,
                                            ),
                                          )
                                        : const Text(
                                            'Sign in with Email',
                                            style: TextStyle(
                                              fontSize: 16,
                                              fontWeight: FontWeight.w600,
                                            ),
                                          ),
                                  ),
                                ),
                                const SizedBox(height: 12),
                                Row(
                                  children: [
                                    Expanded(
                                      child: Divider(color: Colors.grey[300]),
                                    ),
                                    Padding(
                                      padding: const EdgeInsets.symmetric(
                                          horizontal: 12),
                                      child: Text(
                                        'or',
                                        style: TextStyle(
                                          color: Colors.grey[600],
                                          fontSize: 13,
                                        ),
                                      ),
                                    ),
                                    Expanded(
                                      child: Divider(color: Colors.grey[300]),
                                    ),
                                  ],
                                ),
                                const SizedBox(height: 12),
                                SizedBox(
                                  width: double.infinity,
                                  height: 48,
                                  child: OutlinedButton.icon(
                                    onPressed: provider.isLoading
                                        ? null
                                        : _signInWithGoogle,
                                    style: OutlinedButton.styleFrom(
                                      shape: RoundedRectangleBorder(
                                        borderRadius: BorderRadius.circular(12),
                                      ),
                                      side: BorderSide(
                                        color: Colors.grey[300]!,
                                        width: 1.5,
                                      ),
                                    ),
                                    icon: const Icon(Icons.login),
                                    label: const Text(
                                      'Continue with Google',
                                      style: TextStyle(
                                        fontSize: 16,
                                        fontWeight: FontWeight.w600,
                                      ),
                                    ),
                                  ),
                                ),
                              ],
                            );
                          },
                        ),
                      ],
                    );
                  }

                  return Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        'Welcome back!',
                        style: Theme.of(context)
                            .textTheme
                            .headlineMedium
                            ?.copyWith(
                              fontWeight: FontWeight.w700,
                              letterSpacing: -0.5,
                            ),
                      ),
                      const SizedBox(height: 24),
                      Container(
                        padding: const EdgeInsets.all(16),
                        decoration: BoxDecoration(
                          color: Theme.of(context)
                              .colorScheme
                              .surfaceContainerHighest,
                          borderRadius: BorderRadius.circular(16),
                          border: Border.all(
                            color: Theme.of(context)
                                .colorScheme
                                .outline
                                .withValues(alpha: 0.2),
                          ),
                        ),
                        child: Row(
                          children: [
                            CircleAvatar(
                              radius: 28,
                              backgroundImage: user.photoUrl != null
                                  ? NetworkImage(user.photoUrl!)
                                  : null,
                              backgroundColor: Theme.of(context)
                                  .colorScheme
                                  .primaryContainer,
                              child: user.photoUrl == null
                                  ? Text(
                                      user.email
                                              ?.substring(0, 1)
                                              .toUpperCase() ??
                                          'U',
                                      style: const TextStyle(
                                        fontSize: 18,
                                        fontWeight: FontWeight.w600,
                                      ),
                                    )
                                  : null,
                            ),
                            const SizedBox(width: 16),
                            Expanded(
                              child: Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: [
                                  Text(
                                    user.displayName ?? user.email ?? 'User',
                                    style: Theme.of(context)
                                        .textTheme
                                        .bodyLarge
                                        ?.copyWith(
                                          fontWeight: FontWeight.w600,
                                        ),
                                  ),
                                  const SizedBox(height: 4),
                                  Text(
                                    'Logged in via ${user.provider}',
                                    style: Theme.of(context)
                                        .textTheme
                                        .bodySmall
                                        ?.copyWith(
                                          color: Colors.grey[600],
                                        ),
                                  ),
                                ],
                              ),
                            ),
                          ],
                        ),
                      ),
                      const SizedBox(height: 24),
                      SizedBox(
                        width: double.infinity,
                        height: 48,
                        child: ElevatedButton.icon(
                          onPressed: _signOut,
                          icon: const Icon(Icons.logout),
                          label: const Text('Sign Out'),
                          style: ElevatedButton.styleFrom(
                            backgroundColor: Colors.red[50],
                            foregroundColor: Colors.red[700],
                            shape: RoundedRectangleBorder(
                              borderRadius: BorderRadius.circular(12),
                            ),
                          ),
                        ),
                      ),
                    ],
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}
0
likes
60
points
23
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A simple and modular Firebase authentication wrapper for Flutter, supporting email/password, Google, and Apple sign-in.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

cupertino_icons, firebase_auth, firebase_core, flutter, google_sign_in, provider, sign_in_with_apple

More

Packages that depend on hng_firebase_auth