hng_firebase_auth 1.0.1
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.
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),
),
),
),
),
],
);
},
),
],
),
),
),
);
}
}