diff --git a/examples/firebase_login/.gitignore b/examples/firebase_login/.gitignore new file mode 100644 index 0000000000..7a7a6318a5 --- /dev/null +++ b/examples/firebase_login/.gitignore @@ -0,0 +1,49 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# Firebase configurations +firebase.json +firebase_options.dart diff --git a/examples/firebase_login/.metadata b/examples/firebase_login/.metadata new file mode 100644 index 0000000000..6f1fe90616 --- /dev/null +++ b/examples/firebase_login/.metadata @@ -0,0 +1,42 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 + base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 + - platform: android + create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 + base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 + - platform: ios + create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 + base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 + - platform: macos + create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 + base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 + - platform: web + create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 + base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 + - platform: windows + create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 + base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/examples/firebase_login/README.md b/examples/firebase_login/README.md new file mode 100644 index 0000000000..5cd44cc4d0 --- /dev/null +++ b/examples/firebase_login/README.md @@ -0,0 +1,28 @@ +# Firebase Login + +Example flutter app built with Riverpod that demonstrates authentication with Firebase. + + + + + + + + +
demoerrorsignuphome
+ +## Features + +- Sign up with Email and Password +- Sign in with Email and Password +- Sign in with Google +- Error handling + +## Getting Started + +1. Generate the native folders first by running `flutter create --platforms=android,ios,web,windows,macos .` +2. Create your firebase project, and enable email/password authentication and google. +3. Use [FlutterFire CLI](https://firebase.google.com/docs/flutter/setup?platform=ios) to connect the app with the firebase project. +4. Follow the [platform integration steps](https://pub.dev/packages/google_sign_in#platform-integration) for the `google_sign_in` package. +5. Run the app. +6. To run the tests, run `flutter test`. diff --git a/examples/firebase_login/analysis_options.yaml b/examples/firebase_login/analysis_options.yaml new file mode 100644 index 0000000000..9abd469f87 --- /dev/null +++ b/examples/firebase_login/analysis_options.yaml @@ -0,0 +1,7 @@ +include: ../analysis_options.yaml +# enable riverpod_lint +analyzer: + errors: + prefer_relative_imports: ignore + plugins: + - custom_lint diff --git a/examples/firebase_login/lib/auth/auth_repository.dart b/examples/firebase_login/lib/auth/auth_repository.dart new file mode 100644 index 0000000000..0a864df1fe --- /dev/null +++ b/examples/firebase_login/lib/auth/auth_repository.dart @@ -0,0 +1,301 @@ +import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; +import 'package:firebase_login/auth/user.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'auth_repository.g.dart'; + +/// {@template sign_up_with_email_and_password_failure} +/// Thrown during the sign-up process if a failure occurs. +/// +/// * Check the [Reference API][ref link] for updated error codes. +/// +/// [ref link]: https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/createUserWithEmailAndPassword.html +/// {@endtemplate} +class SignUpWithEmailAndPasswordFailure implements Exception { + /// {@macro sign_up_with_email_and_password_failure} + const SignUpWithEmailAndPasswordFailure([ + this.message = 'An unknown error occurred.', + ]); + + /// Creates a failure message from a [firebase_auth.FirebaseAuthException] code. + /// + /// {@macro sign_up_with_email_and_password_failure} + factory SignUpWithEmailAndPasswordFailure.fromCode(String code) { + switch (code) { + case 'email-already-in-use': + return const SignUpWithEmailAndPasswordFailure( + 'An account already exists for this email address.', + ); + case 'invalid-email': + return const SignUpWithEmailAndPasswordFailure( + 'The email is not valid or badly formatted.', + ); + case 'operation-not-allowed': + return const SignUpWithEmailAndPasswordFailure( + 'This operation is not allowed. Please contact support.', + ); + case 'weak-password': + return const SignUpWithEmailAndPasswordFailure( + 'The password provided is too weak. Please use a stronger password.', + ); + + default: + return const SignUpWithEmailAndPasswordFailure(); + } + } + + /// The associated error message. + final String message; + + @override + String toString() => message; +} + +/// {@template sign_in_with_email_and_password_failure} +/// Thrown during the sign in process if a failure occurs. +/// +/// * Check the [Reference API][ref link] for updated error codes. +/// +/// [ref link]: https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/signInWithEmailAndPassword.html +/// {@endtemplate} +class SignInWithEmailAndPasswordFailure implements Exception { + /// {@macro sign_in_with_email_and_password_failure} + const SignInWithEmailAndPasswordFailure([ + this.message = 'An unknown error occurred.', + ]); + + /// Creates a failure message from a [firebase_auth.FirebaseAuthException] code. + /// + /// {@macro sign_in_with_email_and_password_failure} + factory SignInWithEmailAndPasswordFailure.fromCode(String code) { + switch (code) { + case 'invalid-email': + return const SignInWithEmailAndPasswordFailure( + 'The email is not valid or badly formatted.', + ); + case 'user-disabled': + return const SignInWithEmailAndPasswordFailure( + 'This user account has been disabled. Please contact support.', + ); + case 'user-not-found': + return const SignInWithEmailAndPasswordFailure( + 'No user found with this email. Please check the email or sign up.', + ); + case 'wrong-password': + return const SignInWithEmailAndPasswordFailure( + 'Incorrect password. Please try again.', + ); + case 'invalid-credential': + return const SignInWithEmailAndPasswordFailure( + 'The credential provided is invalid. Please try again or use a different method.', + ); + + default: + return const SignInWithEmailAndPasswordFailure(); + } + } + + /// The associated error message. + final String message; + + @override + String toString() => message; +} + +/// {@template sign_in_with_google_failure} +/// Thrown during the sign in process if a failure occurs. +/// +/// * Check the [Reference API][ref link] for updated error codes. +/// +/// [ref link]: https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/signInWithCredential.html +/// {@endtemplate} +class SignInWithGoogleFailure implements Exception { + /// {@macro sign_in_with_google_failure} + const SignInWithGoogleFailure([ + this.message = 'An unknown error occurred.', + ]); + + /// Creates a failure message from a [firebase_auth.FirebaseAuthException] code. + /// + /// {@macro sign_in_with_google_failure} + factory SignInWithGoogleFailure.fromCode(String code) { + switch (code) { + case 'account-exists-with-different-credential': + return const SignInWithGoogleFailure( + 'An account already exists with a different credential. Please use a different sign-in method.', + ); + case 'invalid-credential': + return const SignInWithGoogleFailure( + 'The credential provided is malformed or has expired.', + ); + case 'user-disabled': + return const SignInWithGoogleFailure( + 'This user account has been disabled. Please contact support.', + ); + case 'user-not-found': + return const SignInWithGoogleFailure( + 'No user found with this credential. Please check your credentials or sign up.', + ); + case 'wrong-password': + return const SignInWithGoogleFailure( + 'Incorrect password. Please try again.', + ); + case 'invalid-verification-code': + return const SignInWithGoogleFailure( + 'The verification code is invalid. Please check and try again.', + ); + case 'invalid-verification-id': + return const SignInWithGoogleFailure( + 'The verification ID is invalid. Please restart the verification process.', + ); + + default: + return const SignInWithGoogleFailure(); + } + } + + /// The associated error message. + final String message; + + @override + String toString() => message; +} + +/// Thrown during the logout process if a failure occurs. +class SignOutFailure implements Exception {} + +/// {@template auth_repository} +/// Repository that handles authentication with email-password +/// and google sign in. +/// {@endtemplate} +class AuthRepository { + /// {@macro auth_repository} + AuthRepository({ + firebase_auth.FirebaseAuth? firebaseAuth, + GoogleSignIn? googleSignIn, + }) : _firebaseAuth = firebaseAuth ?? firebase_auth.FirebaseAuth.instance, + _googleSignIn = googleSignIn ?? GoogleSignIn(); + + final firebase_auth.FirebaseAuth _firebaseAuth; + final GoogleSignIn _googleSignIn; + + /// Stream of [User] that notifies about changes to the user's + /// authentication state (such as sign-in or sign-out) + /// + /// Emits empty user, if the user is unauthenticated. + Stream get user { + return _firebaseAuth.authStateChanges().map((firebaseUser) { + return firebaseUser != null ? firebaseUser.toUser : User.empty(); + }); + } + + /// Creates a new user with the provided email address and password. + /// + /// Throws [SignUpWithEmailAndPasswordFailure] when an exception + /// occurs during the operation. + Future signUpWithEmailAndPassword({ + required String email, + required String password, + }) async { + try { + await _firebaseAuth.createUserWithEmailAndPassword( + email: email, + password: password, + ); + } on firebase_auth.FirebaseAuthException catch (e) { + throw SignUpWithEmailAndPasswordFailure.fromCode(e.code); + } catch (_) { + throw const SignUpWithEmailAndPasswordFailure(); + } + } + + /// Signs in a user with the provided email address and password. + /// + /// Throws [SignInWithEmailAndPasswordFailure] when an exception + /// occurs during the operation. + Future signInWithEmailAndPassword({ + required String email, + required String password, + }) async { + try { + await _firebaseAuth.signInWithEmailAndPassword( + email: email, + password: password, + ); + } on firebase_auth.FirebaseAuthException catch (e) { + throw SignInWithEmailAndPasswordFailure.fromCode(e.code); + } catch (_) { + throw const SignInWithEmailAndPasswordFailure(); + } + } + + /// Triggers the google sign in flow. + /// + /// Throws [SignInWithGoogleFailure] when an exception occurs + /// during the operation. + Future signInWithGoogle() async { + try { + late final firebase_auth.AuthCredential credential; + if (kIsWeb) { + final googleProvider = firebase_auth.GoogleAuthProvider(); + final userCredential = await _firebaseAuth.signInWithPopup( + googleProvider, + ); + credential = userCredential.credential!; + } else { + final googleAccount = await _googleSignIn.signIn(); + final googleAuthentication = await googleAccount!.authentication; + credential = firebase_auth.GoogleAuthProvider.credential( + accessToken: googleAuthentication.accessToken, + idToken: googleAuthentication.idToken, + ); + } + + await _firebaseAuth.signInWithCredential(credential); + } on firebase_auth.FirebaseAuthException catch (e) { + throw SignInWithGoogleFailure.fromCode(e.code); + } catch (_) { + throw const SignInWithGoogleFailure(); + } + } + + /// Signs out currently signed in user. + /// + /// Throws [SignOutFailure] when an exception occurs during + /// the operation. + Future signOut() async { + try { + await Future.wait([ + _firebaseAuth.signOut(), + _googleSignIn.signOut(), + ]); + } catch (_) { + throw SignOutFailure(); + } + } +} + +extension on firebase_auth.User { + /// Maps a [firebase_auth.User] into a [User]. + User get toUser { + return User(id: uid, email: email, name: displayName, photo: photoURL); + } +} + +@riverpod +firebase_auth.FirebaseAuth firebaseAuth(Ref ref) => + firebase_auth.FirebaseAuth.instance; + +@riverpod +GoogleSignIn googleSignIn(Ref ref) => GoogleSignIn(); + +@riverpod +AuthRepository authRepository(Ref ref) { + return AuthRepository( + firebaseAuth: ref.read(firebaseAuthProvider), + googleSignIn: ref.read(googleSignInProvider), + ); +} diff --git a/examples/firebase_login/lib/auth/auth_repository.g.dart b/examples/firebase_login/lib/auth/auth_repository.g.dart new file mode 100644 index 0000000000..0a6479d3e4 --- /dev/null +++ b/examples/firebase_login/lib/auth/auth_repository.g.dart @@ -0,0 +1,60 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auth_repository.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$firebaseAuthHash() => r'073d2de7c8941748647f37dbb00de1c08ef8758b'; + +/// See also [firebaseAuth]. +@ProviderFor(firebaseAuth) +final firebaseAuthProvider = + AutoDisposeProvider.internal( + firebaseAuth, + name: r'firebaseAuthProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$firebaseAuthHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef FirebaseAuthRef = AutoDisposeProviderRef; +String _$googleSignInHash() => r'0b3da4c5bf629e3f7401a2a78c79cccd40689ce1'; + +/// See also [googleSignIn]. +@ProviderFor(googleSignIn) +final googleSignInProvider = AutoDisposeProvider.internal( + googleSignIn, + name: r'googleSignInProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$googleSignInHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef GoogleSignInRef = AutoDisposeProviderRef; +String _$authRepositoryHash() => r'e962172aeebc941ae9bb814668c8a82aa4a94225'; + +/// See also [authRepository]. +@ProviderFor(authRepository) +final authRepositoryProvider = AutoDisposeProvider.internal( + authRepository, + name: r'authRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$authRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef AuthRepositoryRef = AutoDisposeProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/examples/firebase_login/lib/auth/user.dart b/examples/firebase_login/lib/auth/user.dart new file mode 100644 index 0000000000..dbcb7ade9b --- /dev/null +++ b/examples/firebase_login/lib/auth/user.dart @@ -0,0 +1,36 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'user.freezed.dart'; + +/// {@template user} +/// A user model. +/// {@endtemplate} +@freezed +class User with _$User { + /// {@macro user} + const factory User({ + /// Unique identifier for the user. + required String id, + + /// The associated email address of the user. + String? email, + + /// Name of the user. + String? name, + + /// Display photo URL of the user. + String? photo, + }) = _User; + + /// {@macro user} + const User._(); + + /// Represents an unauthenticated user. + factory User.empty() => const User(id: ''); + + /// Whether the user is unauthenticated. + bool get isUnauthenticated => this == User.empty(); + + /// Whether the user is authenticated. + bool get isAuthenticated => !isUnauthenticated; +} diff --git a/examples/firebase_login/lib/auth/user.freezed.dart b/examples/firebase_login/lib/auth/user.freezed.dart new file mode 100644 index 0000000000..c5dccd5dd5 --- /dev/null +++ b/examples/firebase_login/lib/auth/user.freezed.dart @@ -0,0 +1,214 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'user.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$User { + /// Unique identifier for the user. + String get id => throw _privateConstructorUsedError; + + /// The associated email address of the user. + String? get email => throw _privateConstructorUsedError; + + /// Name of the user. + String? get name => throw _privateConstructorUsedError; + + /// Display photo URL of the user. + String? get photo => throw _privateConstructorUsedError; + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserCopyWith<$Res> { + factory $UserCopyWith(User value, $Res Function(User) then) = + _$UserCopyWithImpl<$Res, User>; + @useResult + $Res call({String id, String? email, String? name, String? photo}); +} + +/// @nodoc +class _$UserCopyWithImpl<$Res, $Val extends User> + implements $UserCopyWith<$Res> { + _$UserCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? email = freezed, + Object? name = freezed, + Object? photo = freezed, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + photo: freezed == photo + ? _value.photo + : photo // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UserImplCopyWith<$Res> implements $UserCopyWith<$Res> { + factory _$$UserImplCopyWith( + _$UserImpl value, $Res Function(_$UserImpl) then) = + __$$UserImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String id, String? email, String? name, String? photo}); +} + +/// @nodoc +class __$$UserImplCopyWithImpl<$Res> + extends _$UserCopyWithImpl<$Res, _$UserImpl> + implements _$$UserImplCopyWith<$Res> { + __$$UserImplCopyWithImpl(_$UserImpl _value, $Res Function(_$UserImpl) _then) + : super(_value, _then); + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? email = freezed, + Object? name = freezed, + Object? photo = freezed, + }) { + return _then(_$UserImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + photo: freezed == photo + ? _value.photo + : photo // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc + +class _$UserImpl extends _User { + const _$UserImpl({required this.id, this.email, this.name, this.photo}) + : super._(); + + /// Unique identifier for the user. + @override + final String id; + + /// The associated email address of the user. + @override + final String? email; + + /// Name of the user. + @override + final String? name; + + /// Display photo URL of the user. + @override + final String? photo; + + @override + String toString() { + return 'User(id: $id, email: $email, name: $name, photo: $photo)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.email, email) || other.email == email) && + (identical(other.name, name) || other.name == name) && + (identical(other.photo, photo) || other.photo == photo)); + } + + @override + int get hashCode => Object.hash(runtimeType, id, email, name, photo); + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserImplCopyWith<_$UserImpl> get copyWith => + __$$UserImplCopyWithImpl<_$UserImpl>(this, _$identity); +} + +abstract class _User extends User { + const factory _User( + {required final String id, + final String? email, + final String? name, + final String? photo}) = _$UserImpl; + const _User._() : super._(); + + /// Unique identifier for the user. + @override + String get id; + + /// The associated email address of the user. + @override + String? get email; + + /// Name of the user. + @override + String? get name; + + /// Display photo URL of the user. + @override + String? get photo; + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserImplCopyWith<_$UserImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/examples/firebase_login/lib/home/home.dart b/examples/firebase_login/lib/home/home.dart new file mode 100644 index 0000000000..128a0e677a --- /dev/null +++ b/examples/firebase_login/lib/home/home.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../auth/auth_repository.dart'; +import '../main.dart'; + +class HomePage extends ConsumerWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final authProvider = ref.read(authRepositoryProvider); + final user = ref.watch(userProvider).value; + + return Scaffold( + appBar: AppBar( + title: const Text('Home Page'), + actions: [ + IconButton( + onPressed: authProvider.signOut, + icon: const Icon(Icons.logout), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.only(bottom: 2 * kToolbarHeight), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16, + children: [ + Text('User ID: ${user?.id ?? ''}'), + Text('Name: ${user?.name ?? ''}'), + Text('Email: ${user?.email ?? ''}'), + ], + ), + ), + ), + ); + } +} diff --git a/examples/firebase_login/lib/main.dart b/examples/firebase_login/lib/main.dart new file mode 100644 index 0000000000..323e47de11 --- /dev/null +++ b/examples/firebase_login/lib/main.dart @@ -0,0 +1,54 @@ +import 'dart:async'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'auth/auth_repository.dart'; +import 'auth/user.dart'; +import 'firebase_options.dart'; +import 'home/home.dart'; +import 'sign_in/sign_in.dart'; + +part 'main.g.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + runApp( + const ProviderScope( + child: App(), + ), + ); +} + +class App extends ConsumerWidget { + const App({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final user = ref.watch(userProvider).value; + + return MaterialApp( + title: 'Riverpod - Firebase Login Example', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo), + useMaterial3: false, + ), + home: user == null + ? const SignInPage() + : user.isAuthenticated + ? const HomePage() + : const SignInPage(), + ); + } +} + +@riverpod +Stream user(Ref ref) { + return ref.watch(authRepositoryProvider).user; +} diff --git a/examples/firebase_login/lib/main.g.dart b/examples/firebase_login/lib/main.g.dart new file mode 100644 index 0000000000..3c2ae50021 --- /dev/null +++ b/examples/firebase_login/lib/main.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'main.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$userHash() => r'3e037008e28e00fe8b6f74b7f3c60209ef6f0624'; + +/// See also [user]. +@ProviderFor(user) +final userProvider = AutoDisposeStreamProvider.internal( + user, + name: r'userProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$userHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef UserRef = AutoDisposeStreamProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/examples/firebase_login/lib/sign_in/sign_in.dart b/examples/firebase_login/lib/sign_in/sign_in.dart new file mode 100644 index 0000000000..44f7b2fbd0 --- /dev/null +++ b/examples/firebase_login/lib/sign_in/sign_in.dart @@ -0,0 +1,184 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../auth/auth_repository.dart'; +import '../sign_up/sign_up.dart'; + +part 'sign_in.freezed.dart'; + +class SignInPage extends ConsumerWidget { + const SignInPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(signInProvider); + final stateNotifier = ref.watch(signInProvider.notifier); + + return Scaffold( + appBar: AppBar( + title: const Text('Sign in to your account'), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + spacing: 16, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SignInForm(), + ElevatedButton( + key: const Key('signInWithGoogle_elevatedButton'), + onPressed: state.isFormSubmitting + ? null + : stateNotifier.signInWithGoogle, + child: state.isFormSubmitting + ? const CircularProgressIndicator.adaptive() + : const Text('Sign in with google'), + ), + TextButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const SignUpPage(), + ), + ), + child: const Text('Do not have an account? Register'), + ), + state.maybeMap( + failure: (failure) => Text( + failure.message, + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, + ), + orElse: () => const SizedBox.shrink(), + ), + ], + ), + ), + ); + } +} + +class SignInForm extends ConsumerStatefulWidget { + const SignInForm({super.key}); + @override + ConsumerState createState() => _SignInFormState(); +} + +class _SignInFormState extends ConsumerState { + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final state = ref.watch(signInProvider); + final stateNotifier = ref.watch(signInProvider.notifier); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 16, + children: [ + TextFormField( + key: const Key('email_textFormField'), + controller: _emailController, + decoration: const InputDecoration( + labelText: 'Email address', + border: OutlineInputBorder(), + ), + ), + TextFormField( + key: const Key('password_textFormField'), + controller: _passwordController, + obscureText: true, + decoration: const InputDecoration( + labelText: 'Password', + border: OutlineInputBorder(), + ), + ), + ElevatedButton( + key: const Key('signInWithEmailAndPassword_elevatedButton'), + onPressed: state.isFormSubmitting + ? null + : () => stateNotifier.signInWithEmailAndPassword( + email: _emailController.text, + password: _passwordController.text, + ), + child: state.isFormSubmitting + ? const CircularProgressIndicator.adaptive() + : const Text('Sign in to your account'), + ), + ], + ); + } +} + +final signInProvider = StateNotifierProvider( + (ref) => SignInStateNotifier( + authRepository: ref.read(authRepositoryProvider), + ), +); + +@freezed +sealed class SignInState with _$SignInState { + const factory SignInState.initial() = _Initial; + const factory SignInState.loading() = _Loading; + const factory SignInState.success() = _Success; + const factory SignInState.failure(String message) = _Failure; +} + +class SignInStateNotifier extends StateNotifier { + SignInStateNotifier({ + required AuthRepository authRepository, + }) : _authRepository = authRepository, + super(const SignInState.initial()); + + final AuthRepository _authRepository; + + Future signInWithEmailAndPassword({ + required String email, + required String password, + }) async { + state = const SignInState.loading(); + try { + await _authRepository.signInWithEmailAndPassword( + email: email, + password: password, + ); + state = const SignInState.success(); + } on SignInWithEmailAndPasswordFailure catch (e) { + state = SignInState.failure(e.message); + } catch (_) { + state = const SignInState.failure('An unexpected error occured!'); + } + } + + Future signInWithGoogle() async { + state = const SignInState.loading(); + try { + await _authRepository.signInWithGoogle(); + state = const SignInState.success(); + } on SignInWithGoogleFailure catch (e) { + state = SignInState.failure(e.message); + } catch (_) { + state = const SignInState.failure('An unexpected error occured!'); + } + } +} + +extension SignInStateExtension on SignInState { + bool get isFormSubmitting { + return switch (this) { + SignInState.loading => true, + _ => false, + }; + } +} diff --git a/examples/firebase_login/lib/sign_in/sign_in.freezed.dart b/examples/firebase_login/lib/sign_in/sign_in.freezed.dart new file mode 100644 index 0000000000..401e1f94d6 --- /dev/null +++ b/examples/firebase_login/lib/sign_in/sign_in.freezed.dart @@ -0,0 +1,593 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'sign_in.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$SignInState { + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? failure, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Failure value) failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Failure value)? failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Failure value)? failure, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SignInStateCopyWith<$Res> { + factory $SignInStateCopyWith( + SignInState value, $Res Function(SignInState) then) = + _$SignInStateCopyWithImpl<$Res, SignInState>; +} + +/// @nodoc +class _$SignInStateCopyWithImpl<$Res, $Val extends SignInState> + implements $SignInStateCopyWith<$Res> { + _$SignInStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SignInState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$InitialImplCopyWith<$Res> { + factory _$$InitialImplCopyWith( + _$InitialImpl value, $Res Function(_$InitialImpl) then) = + __$$InitialImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$InitialImplCopyWithImpl<$Res> + extends _$SignInStateCopyWithImpl<$Res, _$InitialImpl> + implements _$$InitialImplCopyWith<$Res> { + __$$InitialImplCopyWithImpl( + _$InitialImpl _value, $Res Function(_$InitialImpl) _then) + : super(_value, _then); + + /// Create a copy of SignInState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$InitialImpl implements _Initial { + const _$InitialImpl(); + + @override + String toString() { + return 'SignInState.initial()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$InitialImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) failure, + }) { + return initial(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? failure, + }) { + return initial?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? failure, + required TResult orElse(), + }) { + if (initial != null) { + return initial(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Failure value) failure, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Failure value)? failure, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Failure value)? failure, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class _Initial implements SignInState { + const factory _Initial() = _$InitialImpl; +} + +/// @nodoc +abstract class _$$LoadingImplCopyWith<$Res> { + factory _$$LoadingImplCopyWith( + _$LoadingImpl value, $Res Function(_$LoadingImpl) then) = + __$$LoadingImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LoadingImplCopyWithImpl<$Res> + extends _$SignInStateCopyWithImpl<$Res, _$LoadingImpl> + implements _$$LoadingImplCopyWith<$Res> { + __$$LoadingImplCopyWithImpl( + _$LoadingImpl _value, $Res Function(_$LoadingImpl) _then) + : super(_value, _then); + + /// Create a copy of SignInState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$LoadingImpl implements _Loading { + const _$LoadingImpl(); + + @override + String toString() { + return 'SignInState.loading()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$LoadingImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) failure, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? failure, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? failure, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Failure value) failure, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Failure value)? failure, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Failure value)? failure, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class _Loading implements SignInState { + const factory _Loading() = _$LoadingImpl; +} + +/// @nodoc +abstract class _$$SuccessImplCopyWith<$Res> { + factory _$$SuccessImplCopyWith( + _$SuccessImpl value, $Res Function(_$SuccessImpl) then) = + __$$SuccessImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$SuccessImplCopyWithImpl<$Res> + extends _$SignInStateCopyWithImpl<$Res, _$SuccessImpl> + implements _$$SuccessImplCopyWith<$Res> { + __$$SuccessImplCopyWithImpl( + _$SuccessImpl _value, $Res Function(_$SuccessImpl) _then) + : super(_value, _then); + + /// Create a copy of SignInState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$SuccessImpl implements _Success { + const _$SuccessImpl(); + + @override + String toString() { + return 'SignInState.success()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$SuccessImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) failure, + }) { + return success(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? failure, + }) { + return success?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? failure, + required TResult orElse(), + }) { + if (success != null) { + return success(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Failure value) failure, + }) { + return success(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Failure value)? failure, + }) { + return success?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Failure value)? failure, + required TResult orElse(), + }) { + if (success != null) { + return success(this); + } + return orElse(); + } +} + +abstract class _Success implements SignInState { + const factory _Success() = _$SuccessImpl; +} + +/// @nodoc +abstract class _$$FailureImplCopyWith<$Res> { + factory _$$FailureImplCopyWith( + _$FailureImpl value, $Res Function(_$FailureImpl) then) = + __$$FailureImplCopyWithImpl<$Res>; + @useResult + $Res call({String message}); +} + +/// @nodoc +class __$$FailureImplCopyWithImpl<$Res> + extends _$SignInStateCopyWithImpl<$Res, _$FailureImpl> + implements _$$FailureImplCopyWith<$Res> { + __$$FailureImplCopyWithImpl( + _$FailureImpl _value, $Res Function(_$FailureImpl) _then) + : super(_value, _then); + + /// Create a copy of SignInState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? message = null, + }) { + return _then(_$FailureImpl( + null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$FailureImpl implements _Failure { + const _$FailureImpl(this.message); + + @override + final String message; + + @override + String toString() { + return 'SignInState.failure(message: $message)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$FailureImpl && + (identical(other.message, message) || other.message == message)); + } + + @override + int get hashCode => Object.hash(runtimeType, message); + + /// Create a copy of SignInState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$FailureImplCopyWith<_$FailureImpl> get copyWith => + __$$FailureImplCopyWithImpl<_$FailureImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) failure, + }) { + return failure(message); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? failure, + }) { + return failure?.call(message); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? failure, + required TResult orElse(), + }) { + if (failure != null) { + return failure(message); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Failure value) failure, + }) { + return failure(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Failure value)? failure, + }) { + return failure?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Failure value)? failure, + required TResult orElse(), + }) { + if (failure != null) { + return failure(this); + } + return orElse(); + } +} + +abstract class _Failure implements SignInState { + const factory _Failure(final String message) = _$FailureImpl; + + String get message; + + /// Create a copy of SignInState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$FailureImplCopyWith<_$FailureImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/examples/firebase_login/lib/sign_up/sign_up.dart b/examples/firebase_login/lib/sign_up/sign_up.dart new file mode 100644 index 0000000000..160270a821 --- /dev/null +++ b/examples/firebase_login/lib/sign_up/sign_up.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../auth/auth_repository.dart'; + +part 'sign_up.freezed.dart'; + +class SignUpPage extends ConsumerWidget { + const SignUpPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(signUpProvider); + + ref.listen(signUpProvider, (previous, next) { + next.maybeWhen( + success: () => Navigator.pop(context), + orElse: () {}, + ); + }); + + return Scaffold( + appBar: AppBar( + title: const Text('Create a new account'), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + spacing: 16, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SignUpForm(), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Already have an account? Sign in'), + ), + state.maybeMap( + failure: (failure) => Text( + failure.message, + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, + ), + orElse: () => const SizedBox.shrink(), + ), + ], + ), + ), + ); + } +} + +class SignUpForm extends ConsumerStatefulWidget { + const SignUpForm({super.key}); + @override + ConsumerState createState() => _SignUpFormState(); +} + +class _SignUpFormState extends ConsumerState { + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final state = ref.watch(signUpProvider); + final stateNotifier = ref.watch(signUpProvider.notifier); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 16, + children: [ + TextFormField( + key: const Key('email_textFormField'), + controller: _emailController, + decoration: const InputDecoration( + labelText: 'Email address', + border: OutlineInputBorder(), + ), + ), + TextFormField( + key: const Key('password_textFormField'), + controller: _passwordController, + obscureText: true, + decoration: const InputDecoration( + labelText: 'Password', + border: OutlineInputBorder(), + ), + ), + ElevatedButton( + onPressed: state.isFormSubmitting + ? null + : () => stateNotifier.signUpWithEmailAndPassword( + email: _emailController.text, + password: _passwordController.text, + ), + child: state.isFormSubmitting + ? const CircularProgressIndicator.adaptive() + : const Text('Create your account'), + ), + ], + ); + } +} + +final signUpProvider = StateNotifierProvider( + (ref) => SignUpStateNotifier( + authRepository: ref.read(authRepositoryProvider), + ), +); + +@freezed +sealed class SignUpState with _$SignUpState { + const factory SignUpState.initial() = _Initial; + const factory SignUpState.loading() = _Loading; + const factory SignUpState.success() = _Success; + const factory SignUpState.failure(String message) = _Failure; +} + +class SignUpStateNotifier extends StateNotifier { + SignUpStateNotifier({ + required AuthRepository authRepository, + }) : _authRepository = authRepository, + super(const SignUpState.initial()); + + final AuthRepository _authRepository; + + Future signUpWithEmailAndPassword({ + required String email, + required String password, + }) async { + state = const SignUpState.loading(); + try { + await _authRepository.signUpWithEmailAndPassword( + email: email, + password: password, + ); + state = const SignUpState.success(); + } on SignUpWithEmailAndPasswordFailure catch (e) { + state = SignUpState.failure(e.message); + } catch (_) { + state = const SignUpState.failure('An unexpected error occured!'); + } + } +} + +extension SignUpStateExtension on SignUpState { + bool get isFormSubmitting { + return switch (this) { + SignUpState.loading => true, + _ => false, + }; + } +} diff --git a/examples/firebase_login/lib/sign_up/sign_up.freezed.dart b/examples/firebase_login/lib/sign_up/sign_up.freezed.dart new file mode 100644 index 0000000000..8c59615c89 --- /dev/null +++ b/examples/firebase_login/lib/sign_up/sign_up.freezed.dart @@ -0,0 +1,593 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'sign_up.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$SignUpState { + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? failure, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Failure value) failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Failure value)? failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Failure value)? failure, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SignUpStateCopyWith<$Res> { + factory $SignUpStateCopyWith( + SignUpState value, $Res Function(SignUpState) then) = + _$SignUpStateCopyWithImpl<$Res, SignUpState>; +} + +/// @nodoc +class _$SignUpStateCopyWithImpl<$Res, $Val extends SignUpState> + implements $SignUpStateCopyWith<$Res> { + _$SignUpStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SignUpState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$InitialImplCopyWith<$Res> { + factory _$$InitialImplCopyWith( + _$InitialImpl value, $Res Function(_$InitialImpl) then) = + __$$InitialImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$InitialImplCopyWithImpl<$Res> + extends _$SignUpStateCopyWithImpl<$Res, _$InitialImpl> + implements _$$InitialImplCopyWith<$Res> { + __$$InitialImplCopyWithImpl( + _$InitialImpl _value, $Res Function(_$InitialImpl) _then) + : super(_value, _then); + + /// Create a copy of SignUpState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$InitialImpl implements _Initial { + const _$InitialImpl(); + + @override + String toString() { + return 'SignUpState.initial()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$InitialImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) failure, + }) { + return initial(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? failure, + }) { + return initial?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? failure, + required TResult orElse(), + }) { + if (initial != null) { + return initial(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Failure value) failure, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Failure value)? failure, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Failure value)? failure, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class _Initial implements SignUpState { + const factory _Initial() = _$InitialImpl; +} + +/// @nodoc +abstract class _$$LoadingImplCopyWith<$Res> { + factory _$$LoadingImplCopyWith( + _$LoadingImpl value, $Res Function(_$LoadingImpl) then) = + __$$LoadingImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LoadingImplCopyWithImpl<$Res> + extends _$SignUpStateCopyWithImpl<$Res, _$LoadingImpl> + implements _$$LoadingImplCopyWith<$Res> { + __$$LoadingImplCopyWithImpl( + _$LoadingImpl _value, $Res Function(_$LoadingImpl) _then) + : super(_value, _then); + + /// Create a copy of SignUpState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$LoadingImpl implements _Loading { + const _$LoadingImpl(); + + @override + String toString() { + return 'SignUpState.loading()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$LoadingImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) failure, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? failure, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? failure, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Failure value) failure, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Failure value)? failure, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Failure value)? failure, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class _Loading implements SignUpState { + const factory _Loading() = _$LoadingImpl; +} + +/// @nodoc +abstract class _$$SuccessImplCopyWith<$Res> { + factory _$$SuccessImplCopyWith( + _$SuccessImpl value, $Res Function(_$SuccessImpl) then) = + __$$SuccessImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$SuccessImplCopyWithImpl<$Res> + extends _$SignUpStateCopyWithImpl<$Res, _$SuccessImpl> + implements _$$SuccessImplCopyWith<$Res> { + __$$SuccessImplCopyWithImpl( + _$SuccessImpl _value, $Res Function(_$SuccessImpl) _then) + : super(_value, _then); + + /// Create a copy of SignUpState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$SuccessImpl implements _Success { + const _$SuccessImpl(); + + @override + String toString() { + return 'SignUpState.success()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$SuccessImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) failure, + }) { + return success(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? failure, + }) { + return success?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? failure, + required TResult orElse(), + }) { + if (success != null) { + return success(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Failure value) failure, + }) { + return success(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Failure value)? failure, + }) { + return success?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Failure value)? failure, + required TResult orElse(), + }) { + if (success != null) { + return success(this); + } + return orElse(); + } +} + +abstract class _Success implements SignUpState { + const factory _Success() = _$SuccessImpl; +} + +/// @nodoc +abstract class _$$FailureImplCopyWith<$Res> { + factory _$$FailureImplCopyWith( + _$FailureImpl value, $Res Function(_$FailureImpl) then) = + __$$FailureImplCopyWithImpl<$Res>; + @useResult + $Res call({String message}); +} + +/// @nodoc +class __$$FailureImplCopyWithImpl<$Res> + extends _$SignUpStateCopyWithImpl<$Res, _$FailureImpl> + implements _$$FailureImplCopyWith<$Res> { + __$$FailureImplCopyWithImpl( + _$FailureImpl _value, $Res Function(_$FailureImpl) _then) + : super(_value, _then); + + /// Create a copy of SignUpState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? message = null, + }) { + return _then(_$FailureImpl( + null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$FailureImpl implements _Failure { + const _$FailureImpl(this.message); + + @override + final String message; + + @override + String toString() { + return 'SignUpState.failure(message: $message)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$FailureImpl && + (identical(other.message, message) || other.message == message)); + } + + @override + int get hashCode => Object.hash(runtimeType, message); + + /// Create a copy of SignUpState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$FailureImplCopyWith<_$FailureImpl> get copyWith => + __$$FailureImplCopyWithImpl<_$FailureImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) failure, + }) { + return failure(message); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? failure, + }) { + return failure?.call(message); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? failure, + required TResult orElse(), + }) { + if (failure != null) { + return failure(message); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Failure value) failure, + }) { + return failure(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Failure value)? failure, + }) { + return failure?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Failure value)? failure, + required TResult orElse(), + }) { + if (failure != null) { + return failure(this); + } + return orElse(); + } +} + +abstract class _Failure implements SignUpState { + const factory _Failure(final String message) = _$FailureImpl; + + String get message; + + /// Create a copy of SignUpState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$FailureImplCopyWith<_$FailureImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/examples/firebase_login/pubspec.yaml b/examples/firebase_login/pubspec.yaml new file mode 100644 index 0000000000..1e75768d1d --- /dev/null +++ b/examples/firebase_login/pubspec.yaml @@ -0,0 +1,33 @@ +name: firebase_login +description: Example flutter app built with Riverpod that demonstrates + authentication with Firebase. +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=3.0.0 <4.0.0" + +dependencies: + cupertino_icons: ^1.0.8 + firebase_auth: ^5.4.0 + firebase_core: ^3.10.0 + flutter: + sdk: flutter + flutter_riverpod: ^2.6.1 + freezed_annotation: ^2.4.4 + google_sign_in: ^6.2.2 + riverpod_annotation: ^2.6.1 + +dev_dependencies: + build_runner: ^2.4.14 + custom_lint: ^0.7.1 + flutter_lints: ^5.0.0 + flutter_test: + sdk: flutter + freezed: ^2.5.8 + mocktail: ^1.0.4 + riverpod_generator: ^2.6.4 + riverpod_lint: ^2.6.4 + +flutter: + uses-material-design: true diff --git a/examples/firebase_login/pubspec_overrides.yaml b/examples/firebase_login/pubspec_overrides.yaml new file mode 100644 index 0000000000..74695a9c8d --- /dev/null +++ b/examples/firebase_login/pubspec_overrides.yaml @@ -0,0 +1,14 @@ +# melos_managed_dependency_overrides: flutter_riverpod,riverpod,riverpod_lint,riverpod_analyzer_utils +dependency_overrides: + flutter_riverpod: + path: ../../packages/flutter_riverpod + riverpod: + path: ../../packages/riverpod + riverpod_analyzer_utils: + path: ../../packages/riverpod_analyzer_utils + riverpod_lint: + path: ../../packages/riverpod_lint + riverpod_annotation: + path: ../../packages/riverpod_annotation + riverpod_generator: + path: ../../packages/riverpod_generator diff --git a/examples/firebase_login/samples/demo.gif b/examples/firebase_login/samples/demo.gif new file mode 100644 index 0000000000..1a097ab3a9 Binary files /dev/null and b/examples/firebase_login/samples/demo.gif differ diff --git a/examples/firebase_login/samples/error.png b/examples/firebase_login/samples/error.png new file mode 100644 index 0000000000..447966fafd Binary files /dev/null and b/examples/firebase_login/samples/error.png differ diff --git a/examples/firebase_login/samples/home.png b/examples/firebase_login/samples/home.png new file mode 100644 index 0000000000..d0a6924382 Binary files /dev/null and b/examples/firebase_login/samples/home.png differ diff --git a/examples/firebase_login/samples/signup.png b/examples/firebase_login/samples/signup.png new file mode 100644 index 0000000000..ee9258852f Binary files /dev/null and b/examples/firebase_login/samples/signup.png differ diff --git a/examples/firebase_login/test/app_test.dart b/examples/firebase_login/test/app_test.dart new file mode 100644 index 0000000000..fe4ba03e3a --- /dev/null +++ b/examples/firebase_login/test/app_test.dart @@ -0,0 +1,54 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:firebase_login/auth/auth_repository.dart'; +import 'package:firebase_login/auth/user.dart'; +import 'package:firebase_login/home/home.dart'; +import 'package:firebase_login/main.dart'; +import 'package:firebase_login/sign_in/sign_in.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'pump_app.dart'; + +void main() { + late AuthRepository authRepository; + + setUp(() { + authRepository = MockAuthRepository(); + when(() => authRepository.user).thenAnswer((_) => Stream.empty()); + }); + + group('App', () { + testWidgets( + 'renders SignInPage when the user stream is empty', + (tester) async { + await tester.pumpApp(App(), authRepository: authRepository); + expect(find.byType(SignInPage), findsOneWidget); + }, + ); + + testWidgets( + 'renders SignInPage when the user stream emits User.empty', + (tester) async { + when(() => authRepository.user).thenAnswer( + (_) => Stream.value(User.empty()), + ); + await tester.pumpApp(App(), authRepository: authRepository); + await tester.pumpAndSettle(); + expect(find.byType(SignInPage), findsOneWidget); + }, + ); + + testWidgets( + 'renders HomePage when the user stream emits authenticated user', + (tester) async { + when(() => authRepository.user).thenAnswer( + (_) => Stream.value(User(id: 'abc')), + ); + await tester.pumpApp(App(), authRepository: authRepository); + await tester.pumpAndSettle(); + expect(find.byType(HomePage), findsOneWidget); + }, + ); + }); +} diff --git a/examples/firebase_login/test/home_test.dart b/examples/firebase_login/test/home_test.dart new file mode 100644 index 0000000000..c3e82a4ece --- /dev/null +++ b/examples/firebase_login/test/home_test.dart @@ -0,0 +1,29 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:firebase_login/auth/auth_repository.dart'; +import 'package:firebase_login/home/home.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'pump_app.dart'; + +void main() { + late AuthRepository authRepository; + + setUp(() { + authRepository = MockAuthRepository(); + when(() => authRepository.signOut()).thenAnswer((_) => Future.value()); + }); + + group('Home', () { + testWidgets('triggers signOut on icon click', (tester) async { + await tester.pumpApp( + MaterialApp(home: HomePage()), + authRepository: authRepository, + ); + await tester.tap(find.byType(IconButton)); + verify(() => authRepository.signOut()).called(1); + }); + }); +} diff --git a/examples/firebase_login/test/pump_app.dart b/examples/firebase_login/test/pump_app.dart new file mode 100644 index 0000000000..c31a7cdefc --- /dev/null +++ b/examples/firebase_login/test/pump_app.dart @@ -0,0 +1,22 @@ +import 'package:firebase_login/auth/auth_repository.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockAuthRepository extends Mock implements AuthRepository {} + +extension WidgetTesterExtension on WidgetTester { + Future pumpApp(Widget child, {AuthRepository? authRepository}) { + return pumpWidget( + ProviderScope( + overrides: [ + authRepositoryProvider.overrideWithValue( + authRepository ?? MockAuthRepository(), + ), + ], + child: child, + ), + ); + } +} diff --git a/examples/firebase_login/test/sign_in_test.dart b/examples/firebase_login/test/sign_in_test.dart new file mode 100644 index 0000000000..c826a13f0b --- /dev/null +++ b/examples/firebase_login/test/sign_in_test.dart @@ -0,0 +1,67 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:firebase_login/auth/auth_repository.dart'; +import 'package:firebase_login/sign_in/sign_in.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'pump_app.dart'; + +void main() { + late AuthRepository authRepository; + + const emailKey = Key('email_textFormField'); + const passwordKey = Key('password_textFormField'); + const emailSignInKey = Key('signInWithEmailAndPassword_elevatedButton'); + const googleSignInKey = Key('signInWithGoogle_elevatedButton'); + + setUp(() { + authRepository = MockAuthRepository(); + when( + () => authRepository.signInWithEmailAndPassword( + email: any(named: 'email'), + password: any(named: 'password'), + ), + ).thenAnswer((_) async {}); + when(() => authRepository.signInWithGoogle()).thenAnswer((_) async {}); + }); + + group('SignIn', () { + testWidgets( + 'triggers signInWithEmailAndPassword on submit button click', + (tester) async { + await tester.pumpApp( + MaterialApp(home: SignInPage()), + authRepository: authRepository, + ); + + await tester.enterText(find.byKey(emailKey), 'email@email.com'); + await tester.enterText(find.byKey(passwordKey), 's3cRe7'); + + await tester.tap(find.byKey(emailSignInKey)); + verify( + () => authRepository.signInWithEmailAndPassword( + email: 'email@email.com', + password: 's3cRe7', + ), + ).called(1); + }, + ); + + testWidgets( + 'triggers signInWithGoogle on google button click', + (tester) async { + await tester.pumpApp( + MaterialApp(home: SignInPage()), + authRepository: authRepository, + ); + + await tester.tap(find.byKey(googleSignInKey)); + verify( + () => authRepository.signInWithGoogle(), + ).called(1); + }, + ); + }); +} diff --git a/examples/firebase_login/test/sign_up_test.dart b/examples/firebase_login/test/sign_up_test.dart new file mode 100644 index 0000000000..213ee25f57 --- /dev/null +++ b/examples/firebase_login/test/sign_up_test.dart @@ -0,0 +1,49 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:firebase_login/auth/auth_repository.dart'; +import 'package:firebase_login/sign_up/sign_up.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'pump_app.dart'; + +void main() { + late AuthRepository authRepository; + + const emailKey = Key('email_textFormField'); + const passwordKey = Key('password_textFormField'); + + setUp(() { + authRepository = MockAuthRepository(); + when( + () => authRepository.signUpWithEmailAndPassword( + email: any(named: 'email'), + password: any(named: 'password'), + ), + ).thenAnswer((_) async {}); + }); + + group('SignUp', () { + testWidgets( + 'triggers signUpWithEmailAndPassword on submit button click', + (tester) async { + await tester.pumpApp( + MaterialApp(home: SignUpPage()), + authRepository: authRepository, + ); + + await tester.enterText(find.byKey(emailKey), 'email@email.com'); + await tester.enterText(find.byKey(passwordKey), 's3cRe7'); + + await tester.tap(find.byType(ElevatedButton)); + verify( + () => authRepository.signUpWithEmailAndPassword( + email: 'email@email.com', + password: 's3cRe7', + ), + ).called(1); + }, + ); + }); +}