├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── lib ├── authentication │ ├── authentication_view.dart │ ├── controller │ │ ├── authentication_controller.dart │ │ └── authentication_state.dart │ ├── forgot_password │ │ ├── controller │ │ │ ├── forgot_password_controller.dart │ │ │ └── forgot_password_state.dart │ │ └── forgot_password.dart │ ├── google_signin │ │ ├── controller │ │ │ └── google_signin_controller.dart │ │ └── google_signin_button.dart │ ├── signin │ │ ├── controller │ │ │ ├── signin_controller.dart │ │ │ └── signin_state.dart │ │ └── signin.dart │ └── signup │ │ ├── controller │ │ ├── signup_controller.dart │ │ └── signup_state.dart │ │ └── signup.dart ├── components │ ├── auth_switch_button.dart │ └── text_input_field.dart ├── main.dart ├── profile │ └── profile.dart └── repository │ └── auth_repo_provider.dart ├── packages ├── firebase_auth_repository │ ├── analysis_options.yaml │ ├── lib │ │ ├── authentication_repository.dart │ │ └── src │ │ │ ├── auth_user.dart │ │ │ └── authentication_repository.dart │ └── pubspec.yaml └── form_validators │ ├── analysis_options.yaml │ ├── lib │ ├── form_validators.dart │ └── src │ │ ├── email.dart │ │ ├── name.dart │ │ └── password.dart │ └── pubspec.yaml └── pubspec.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | pubspec.lock 35 | /ios 36 | /test 37 | /web 38 | /windows 39 | /android 40 | /linux 41 | /macos 42 | 43 | # Web related 44 | lib/generated_plugin_registrant.dart 45 | 46 | # Symbolication related 47 | app.*.symbols 48 | 49 | # Obfuscation related 50 | app.*.map.json 51 | 52 | # Android Studio will place build artifacts here 53 | /android/app/debug 54 | /android/app/profile 55 | /android/app/release 56 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 17 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 18 | - platform: android 19 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 20 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 21 | - platform: ios 22 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 23 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 24 | - platform: linux 25 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 26 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 27 | - platform: macos 28 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 29 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 30 | - platform: web 31 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 32 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 33 | - platform: windows 34 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 35 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # firebase_authentication_riverpod 2 | 3 | ## Screen recording 👇 4 | 5 | 6 | 7 | ## Watch the tutorial 👇 8 | 9 |
10 | 11 | 12 | 13 |
14 | 15 | ## Resources 16 | 17 | 1. Complete source code 18 | 19 | 1. Figma design 20 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /lib/authentication/authentication_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../components/auth_switch_button.dart'; 3 | import 'signin/signin.dart'; 4 | import 'signup/signup.dart'; 5 | 6 | class AuthenticationView extends StatefulWidget { 7 | const AuthenticationView({Key? key}) : super(key: key); 8 | 9 | @override 10 | State createState() => _AuthenticationViewState(); 11 | } 12 | 13 | class _AuthenticationViewState extends State { 14 | bool _showSignIn = true; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Scaffold( 19 | resizeToAvoidBottomInset: false, 20 | body: Stack( 21 | children: [ 22 | Padding( 23 | padding: const EdgeInsets.fromLTRB(16, 250, 16, 0), 24 | child: _showSignIn ? const SignIn() : const SignUp(), 25 | ), 26 | SafeArea( 27 | child: Padding( 28 | padding: const EdgeInsets.only(left: 16), 29 | child: _showSignIn 30 | ? const Text( 31 | "Welcome Back", 32 | style: TextStyle( 33 | fontSize: 34, 34 | fontWeight: FontWeight.bold, 35 | color: Colors.black87, 36 | ), 37 | ) 38 | : const Text( 39 | "Create Account", 40 | style: TextStyle( 41 | fontSize: 34, 42 | fontWeight: FontWeight.bold, 43 | color: Colors.black87, 44 | ), 45 | ), 46 | ), 47 | ), 48 | AuthSwitchButton( 49 | showSignIn: _showSignIn, 50 | onTap: () { 51 | setState(() { 52 | _showSignIn = !_showSignIn; 53 | }); 54 | }, 55 | ) 56 | ], 57 | ), 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/authentication/controller/authentication_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:authentication_repository/authentication_repository.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | import 'package:equatable/equatable.dart'; 6 | 7 | import '../../repository/auth_repo_provider.dart'; 8 | 9 | part 'authentication_state.dart'; 10 | 11 | final authProvider = StateNotifierProvider( 12 | (ref) => AuthController(ref.watch(authRepoProvider)), 13 | ); 14 | 15 | class AuthController extends StateNotifier { 16 | final AuthenticationRepository _authRepository; 17 | late final StreamSubscription _streamSubscription; 18 | 19 | AuthController(this._authRepository) 20 | : super(const AuthenticationState.unauthenticated()) { 21 | _streamSubscription = 22 | _authRepository.user.listen((user) => _onUserChanged(user)); 23 | } 24 | 25 | void _onUserChanged(AuthUser user) { 26 | if (user.isEmpty) { 27 | state = const AuthenticationState.unauthenticated(); 28 | } else { 29 | state = AuthenticationState.authenticated(user); 30 | } 31 | } 32 | 33 | void onSignOut() { 34 | _authRepository.signOut(); 35 | } 36 | 37 | @override 38 | void dispose() { 39 | _streamSubscription.cancel(); 40 | super.dispose(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/authentication/controller/authentication_state.dart: -------------------------------------------------------------------------------- 1 | part of 'authentication_controller.dart'; 2 | 3 | enum AuthenticationStatus { 4 | authenticated, 5 | unauthenticated, 6 | } 7 | 8 | class AuthenticationState extends Equatable { 9 | final AuthenticationStatus status; 10 | final AuthUser user; 11 | 12 | const AuthenticationState._({ 13 | required this.status, 14 | this.user = AuthUser.empty, 15 | }); 16 | 17 | const AuthenticationState.authenticated(AuthUser user) 18 | : this._(status: AuthenticationStatus.authenticated, user: user); 19 | 20 | const AuthenticationState.unauthenticated() 21 | : this._(status: AuthenticationStatus.unauthenticated); 22 | 23 | @override 24 | List get props => [status, user]; 25 | } 26 | -------------------------------------------------------------------------------- /lib/authentication/forgot_password/controller/forgot_password_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:authentication_repository/authentication_repository.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:form_validators/form_validators.dart'; 5 | import '../../../repository/auth_repo_provider.dart'; 6 | 7 | part 'forgot_password_state.dart'; 8 | 9 | final forgotPasswordProvider = 10 | StateNotifierProvider.autoDispose( 11 | (ref) => ForgotPasswordController( 12 | ref.watch(authRepoProvider), 13 | ), 14 | ); 15 | 16 | class ForgotPasswordController extends StateNotifier { 17 | final AuthenticationRepository _authenticationRepository; 18 | 19 | ForgotPasswordController(this._authenticationRepository) 20 | : super(const ForgotPasswordState()); 21 | 22 | void onEmailChange(String value) { 23 | final email = Email.dirty(value); 24 | 25 | state = state.copyWith( 26 | email: email, 27 | status: Formz.validate( 28 | [email], 29 | ), 30 | ); 31 | } 32 | 33 | Future forgotPassword() async { 34 | if (!state.status.isValidated) return; 35 | state = state.copyWith(status: FormzStatus.submissionInProgress); 36 | try { 37 | await _authenticationRepository.forgotPassword(email: state.email.value); 38 | state = state.copyWith(status: FormzStatus.submissionSuccess); 39 | } on ForgotPasswordFailure catch (e) { 40 | state = state.copyWith( 41 | status: FormzStatus.submissionFailure, errorMessage: e.code); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/authentication/forgot_password/controller/forgot_password_state.dart: -------------------------------------------------------------------------------- 1 | part of 'forgot_password_controller.dart'; 2 | 3 | class ForgotPasswordState extends Equatable { 4 | final Email email; 5 | final FormzStatus status; 6 | final String? errorMessage; 7 | 8 | const ForgotPasswordState({ 9 | this.email = const Email.pure(), 10 | this.status = FormzStatus.pure, 11 | this.errorMessage, 12 | }); 13 | 14 | ForgotPasswordState copyWith({ 15 | Email? email, 16 | FormzStatus? status, 17 | String? errorMessage, 18 | }) { 19 | return ForgotPasswordState( 20 | email: email ?? this.email, 21 | status: status ?? this.status, 22 | errorMessage: errorMessage ?? this.errorMessage, 23 | ); 24 | } 25 | 26 | @override 27 | List get props => [ 28 | email, 29 | status, 30 | ]; 31 | } 32 | -------------------------------------------------------------------------------- /lib/authentication/forgot_password/forgot_password.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:form_validators/form_validators.dart'; 5 | 6 | import 'controller/forgot_password_controller.dart'; 7 | 8 | import '../../components/text_input_field.dart'; 9 | 10 | class ForgotPasswordView extends ConsumerWidget { 11 | const ForgotPasswordView({Key? key}) : super(key: key); 12 | 13 | String _getButtonText(FormzStatus status) { 14 | if (status.isSubmissionInProgress) { 15 | return "requesting"; 16 | } else if (status.isSubmissionFailure) { 17 | return "failed"; 18 | } else if (status.isSubmissionSuccess) { 19 | return "done ✅"; 20 | } else { 21 | return "request"; 22 | } 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context, WidgetRef ref) { 27 | final forgotPasswordState = ref.watch(forgotPasswordProvider); 28 | final status = forgotPasswordState.status; 29 | 30 | ref.listen( 31 | forgotPasswordProvider, 32 | (previous, current) { 33 | if (current.status.isSubmissionFailure) { 34 | Navigator.of(context).pop(); 35 | ScaffoldMessenger.of(context).showSnackBar( 36 | SnackBar( 37 | content: Text("${current.errorMessage}"), 38 | ), 39 | ); 40 | } 41 | }, 42 | ); 43 | 44 | return Scaffold( 45 | body: Center( 46 | child: Padding( 47 | padding: const EdgeInsets.all(16.0), 48 | child: Column( 49 | mainAxisAlignment: MainAxisAlignment.center, 50 | children: [ 51 | TextInputField( 52 | hintText: "Please enter your Email", 53 | errorText: Email.showEmailErrorMessage( 54 | forgotPasswordState.email.error), 55 | onChanged: (email) { 56 | ref 57 | .read(forgotPasswordProvider.notifier) 58 | .onEmailChange(email); 59 | }, 60 | ), 61 | Row( 62 | mainAxisAlignment: MainAxisAlignment.center, 63 | children: [ 64 | TextButton( 65 | onPressed: status.isSubmissionInProgress 66 | ? null 67 | : () { 68 | Navigator.of(context).pop(); 69 | }, 70 | child: const Text("Cancel"), 71 | ), 72 | TextButton( 73 | onPressed: status.isSubmissionInProgress || 74 | status.isSubmissionSuccess 75 | ? null 76 | : () { 77 | ref 78 | .read(forgotPasswordProvider.notifier) 79 | .forgotPassword(); 80 | }, 81 | child: Text(_getButtonText(status)), 82 | ), 83 | ], 84 | ) 85 | ], 86 | ), 87 | ), 88 | ), 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/authentication/google_signin/controller/google_signin_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:authentication_repository/authentication_repository.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import '../../../repository/auth_repo_provider.dart'; 4 | 5 | final googleSignInProvider = 6 | StateNotifierProvider( 7 | (ref) { 8 | final authenticationRepository = ref.watch(authRepoProvider); 9 | return GoogleSignInController(authenticationRepository); 10 | }, 11 | ); 12 | 13 | enum GoogleSignInState { 14 | initial, 15 | loading, 16 | success, 17 | error, 18 | } 19 | 20 | class GoogleSignInController extends StateNotifier { 21 | final AuthenticationRepository _authenticationRepository; 22 | 23 | GoogleSignInController(this._authenticationRepository) 24 | : super(GoogleSignInState.initial); 25 | 26 | Future signInWithGoogle() async { 27 | state = GoogleSignInState.loading; 28 | 29 | try { 30 | final isNewUser = await _authenticationRepository.signInWithGoogle(); 31 | 32 | if (isNewUser != null && isNewUser) { 33 | // white to database 34 | // call cloud firestore repository 35 | } 36 | 37 | state = GoogleSignInState.success; 38 | } on SignInWithGoogleFailure catch (_) { 39 | state = GoogleSignInState.error; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/authentication/google_signin/google_signin_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'controller/google_signin_controller.dart'; 4 | 5 | class GoogleSignInButton extends ConsumerWidget { 6 | const GoogleSignInButton({Key? key}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context, WidgetRef ref) { 10 | ref.listen( 11 | googleSignInProvider, 12 | (previous, current) { 13 | if (current == GoogleSignInState.loading) { 14 | showDialog( 15 | context: context, 16 | builder: (_) => const SimpleDialog( 17 | title: Padding( 18 | padding: EdgeInsets.all(32.0), 19 | child: Text("Loading..."), 20 | ), 21 | ), 22 | ); 23 | } else if (current == GoogleSignInState.error) { 24 | Navigator.of(context).pop(); 25 | 26 | ScaffoldMessenger.of(context).showSnackBar( 27 | const SnackBar( 28 | content: Text("Google signin failed"), 29 | ), 30 | ); 31 | } else { 32 | Navigator.of(context).pop(); 33 | } 34 | }, 35 | ); 36 | 37 | return TextButton( 38 | child: const Text( 39 | "Sign In With Google", 40 | style: TextStyle( 41 | color: Color(0xFF9A9A9A), 42 | fontWeight: FontWeight.bold, 43 | fontSize: 18, 44 | ), 45 | ), 46 | onPressed: () { 47 | ref.read(googleSignInProvider.notifier).signInWithGoogle(); 48 | }, 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/authentication/signin/controller/signin_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:form_validators/form_validators.dart'; 4 | import 'package:authentication_repository/authentication_repository.dart'; 5 | import '../../../../repository/auth_repo_provider.dart'; 6 | part 'signin_state.dart'; 7 | 8 | final signInProvider = 9 | StateNotifierProvider.autoDispose( 10 | (ref) => SignInController(ref.watch(authRepoProvider)), 11 | ); 12 | 13 | class SignInController extends StateNotifier { 14 | final AuthenticationRepository _authenticationRepository; 15 | SignInController(this._authenticationRepository) : super(const SignInState()); 16 | 17 | void onEmailChange(String value) { 18 | final email = Email.dirty(value); 19 | 20 | state = state.copyWith( 21 | email: email, 22 | status: Formz.validate([email, state.password]), 23 | ); 24 | } 25 | 26 | void onPasswordChange(String value) { 27 | final password = Password.dirty(value); 28 | state = state.copyWith( 29 | password: password, 30 | status: Formz.validate([state.email, password]), 31 | ); 32 | } 33 | 34 | void signInWithEmailAndPassword() async { 35 | if (!state.status.isValidated) return; 36 | state = state.copyWith(status: FormzStatus.submissionInProgress); 37 | try { 38 | await _authenticationRepository.signInWithEmailAndPassword( 39 | email: state.email.value, 40 | password: state.password.value, 41 | ); 42 | 43 | state = state.copyWith(status: FormzStatus.submissionSuccess); 44 | } on SignInWithEmailAndPasswordFailure catch (e) { 45 | state = state.copyWith( 46 | status: FormzStatus.submissionFailure, errorMessage: e.code); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/authentication/signin/controller/signin_state.dart: -------------------------------------------------------------------------------- 1 | part of 'signin_controller.dart'; 2 | 3 | class SignInState extends Equatable { 4 | final Email email; 5 | final Password password; 6 | final FormzStatus status; 7 | final String? errorMessage; 8 | 9 | const SignInState({ 10 | this.email = const Email.pure(), 11 | this.password = const Password.pure(), 12 | this.status = FormzStatus.pure, 13 | this.errorMessage, 14 | }); 15 | 16 | SignInState copyWith({ 17 | Email? email, 18 | Password? password, 19 | FormzStatus? status, 20 | bool? buttonTapped, 21 | String? errorMessage, 22 | }) { 23 | return SignInState( 24 | email: email ?? this.email, 25 | password: password ?? this.password, 26 | status: status ?? this.status, 27 | errorMessage: errorMessage ?? this.errorMessage, 28 | ); 29 | } 30 | 31 | @override 32 | List get props => [ 33 | email, 34 | password, 35 | status, 36 | ]; 37 | } 38 | -------------------------------------------------------------------------------- /lib/authentication/signin/signin.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:form_validators/form_validators.dart'; 4 | 5 | import '../../components/text_input_field.dart'; 6 | 7 | import '../forgot_password/forgot_password.dart'; 8 | 9 | import '../google_signin/google_signin_button.dart'; 10 | import 'controller/signin_controller.dart'; 11 | 12 | class SignIn extends ConsumerWidget { 13 | const SignIn({Key? key}) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context, WidgetRef ref) { 17 | ref.listen( 18 | signInProvider, 19 | (previous, current) { 20 | if (current.status.isSubmissionInProgress) { 21 | showDialog( 22 | context: context, 23 | builder: (_) => const SimpleDialog( 24 | title: Padding( 25 | padding: EdgeInsets.all(32.0), 26 | child: Text("Loading..."), 27 | ), 28 | ), 29 | ); 30 | } else if (current.status.isSubmissionFailure) { 31 | Navigator.of(context).pop(); 32 | 33 | ScaffoldMessenger.of(context).showSnackBar( 34 | SnackBar( 35 | content: Text("${current.errorMessage}"), 36 | ), 37 | ); 38 | } else if (current.status.isSubmissionSuccess) { 39 | Navigator.of(context).pop(); 40 | } 41 | }, 42 | ); 43 | return Column( 44 | children: const [ 45 | EmailField(), 46 | SizedBox( 47 | height: 16, 48 | ), 49 | PasswordField(), 50 | SizedBox( 51 | height: 24, 52 | ), 53 | SignInButton(), 54 | Divider(), 55 | GoogleSignInButton(), 56 | ], 57 | ); 58 | } 59 | } 60 | 61 | class EmailField extends ConsumerWidget { 62 | const EmailField({Key? key}) : super(key: key); 63 | 64 | @override 65 | Widget build(BuildContext context, WidgetRef ref) { 66 | final signInState = ref.watch(signInProvider); 67 | final bool showError = signInState.email.invalid; 68 | final signInController = ref.read(signInProvider.notifier); 69 | return TextInputField( 70 | hintText: "Email", 71 | errorText: showError 72 | ? Email.showEmailErrorMessage(signInState.email.error) 73 | : null, 74 | onChanged: (email) => signInController.onEmailChange(email), 75 | ); 76 | } 77 | } 78 | 79 | class PasswordField extends ConsumerWidget { 80 | const PasswordField({Key? key}) : super(key: key); 81 | 82 | @override 83 | Widget build(BuildContext context, WidgetRef ref) { 84 | final signInState = ref.watch(signInProvider); 85 | final bool showError = signInState.password.invalid; 86 | final signInController = ref.read(signInProvider.notifier); 87 | return TextInputField( 88 | hintText: "Password", 89 | obscureText: true, 90 | errorText: showError 91 | ? Password.showPasswordErrorMessage(signInState.password.error) 92 | : null, 93 | onChanged: (password) => signInController.onPasswordChange(password), 94 | ); 95 | } 96 | } 97 | 98 | class ForgotPasswordButton extends StatelessWidget { 99 | const ForgotPasswordButton({Key? key}) : super(key: key); 100 | 101 | @override 102 | Widget build(BuildContext context) { 103 | return TextButton( 104 | onPressed: () { 105 | Navigator.of(context).push( 106 | MaterialPageRoute( 107 | fullscreenDialog: true, 108 | builder: (_) => const ForgotPasswordView(), 109 | ), 110 | ); 111 | }, 112 | child: const Padding( 113 | padding: EdgeInsets.symmetric(vertical: 8.0), 114 | child: Align( 115 | alignment: Alignment.centerRight, 116 | child: Text( 117 | "Forgot Password", 118 | style: TextStyle( 119 | color: Colors.blue, 120 | ), 121 | ), 122 | ), 123 | ), 124 | ); 125 | } 126 | } 127 | 128 | class SignInButton extends ConsumerWidget { 129 | const SignInButton({Key? key}) : super(key: key); 130 | 131 | @override 132 | Widget build(BuildContext context, WidgetRef ref) { 133 | final signInState = ref.watch(signInProvider); 134 | final bool isValidated = signInState.status.isValidated; 135 | final signInController = ref.read(signInProvider.notifier); 136 | return ElevatedButton( 137 | onPressed: isValidated 138 | ? () => signInController.signInWithEmailAndPassword() 139 | : null, 140 | child: const Text( 141 | "Sign In", 142 | ), 143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /lib/authentication/signup/controller/signup_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:form_validators/form_validators.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:equatable/equatable.dart'; 4 | import '../../../../../repository/auth_repo_provider.dart'; 5 | import 'package:authentication_repository/authentication_repository.dart'; 6 | 7 | part 'signup_state.dart'; 8 | 9 | final signUpProvider = 10 | StateNotifierProvider.autoDispose( 11 | (ref) => SignUpController(ref.watch(authRepoProvider)), 12 | ); 13 | 14 | class SignUpController extends StateNotifier { 15 | final AuthenticationRepository _authenticationRepository; 16 | SignUpController(this._authenticationRepository) : super(const SignUpState()); 17 | 18 | void onNameChange(String value) { 19 | final name = Name.dirty(value); 20 | state = state.copyWith( 21 | name: name, 22 | status: Formz.validate([ 23 | name, 24 | state.email, 25 | state.password, 26 | ]), 27 | ); 28 | } 29 | 30 | void onEmailChange(String value) { 31 | final email = Email.dirty(value); 32 | 33 | state = state.copyWith( 34 | email: email, 35 | status: Formz.validate( 36 | [ 37 | state.name, 38 | email, 39 | state.password, 40 | ], 41 | ), 42 | ); 43 | } 44 | 45 | void onPasswordChange(String value) { 46 | final password = Password.dirty(value); 47 | state = state.copyWith( 48 | password: password, 49 | status: Formz.validate( 50 | [ 51 | state.name, 52 | state.email, 53 | password, 54 | ], 55 | ), 56 | ); 57 | } 58 | 59 | void signUpWithEmailAndPassword() async { 60 | if (!state.status.isValidated) return; 61 | state = state.copyWith(status: FormzStatus.submissionInProgress); 62 | try { 63 | await _authenticationRepository.signUpWithEmailAndPassword( 64 | email: state.email.value, 65 | password: state.password.value, 66 | ); 67 | 68 | state = state.copyWith(status: FormzStatus.submissionSuccess); 69 | } on SignUpWithEmailAndPasswordFailure catch (e) { 70 | state = state.copyWith( 71 | status: FormzStatus.submissionFailure, errorMessage: e.code); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/authentication/signup/controller/signup_state.dart: -------------------------------------------------------------------------------- 1 | part of "signup_controller.dart"; 2 | 3 | class SignUpState extends Equatable { 4 | final Name name; 5 | final Email email; 6 | final Password password; 7 | final FormzStatus status; 8 | final String? errorMessage; 9 | 10 | const SignUpState({ 11 | this.name = const Name.pure(), 12 | this.email = const Email.pure(), 13 | this.password = const Password.pure(), 14 | this.status = FormzStatus.pure, 15 | this.errorMessage, 16 | }); 17 | 18 | SignUpState copyWith({ 19 | Name? name, 20 | Email? email, 21 | Password? password, 22 | FormzStatus? status, 23 | String? errorMessage, 24 | }) { 25 | return SignUpState( 26 | name: name ?? this.name, 27 | email: email ?? this.email, 28 | password: password ?? this.password, 29 | status: status ?? this.status, 30 | errorMessage: errorMessage ?? this.errorMessage, 31 | ); 32 | } 33 | 34 | @override 35 | List get props => [ 36 | name, 37 | email, 38 | password, 39 | status, 40 | ]; 41 | } 42 | -------------------------------------------------------------------------------- /lib/authentication/signup/signup.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:form_validators/form_validators.dart'; 4 | 5 | import '../../components/text_input_field.dart'; 6 | 7 | import 'controller/signup_controller.dart'; 8 | 9 | class SignUp extends ConsumerWidget { 10 | const SignUp({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context, WidgetRef ref) { 14 | ref.listen( 15 | signUpProvider, 16 | (previous, current) { 17 | if (current.status.isSubmissionInProgress) { 18 | showDialog( 19 | context: context, 20 | builder: (_) => const SimpleDialog( 21 | title: Padding( 22 | padding: EdgeInsets.all(32.0), 23 | child: Text("Loading..."), 24 | ), 25 | ), 26 | ); 27 | } else if (current.status.isSubmissionFailure) { 28 | Navigator.of(context).pop(); 29 | 30 | ScaffoldMessenger.of(context).showSnackBar( 31 | SnackBar( 32 | content: Text("${current.errorMessage}"), 33 | ), 34 | ); 35 | } else if (current.status.isSubmissionSuccess) { 36 | Navigator.of(context).pop(); 37 | } 38 | }, 39 | ); 40 | 41 | return Column( 42 | children: const [ 43 | NameField(), 44 | SizedBox( 45 | height: 16, 46 | ), 47 | EmailField(), 48 | SizedBox( 49 | height: 16, 50 | ), 51 | PasswordField(), 52 | SizedBox( 53 | height: 24, 54 | ), 55 | SignUpButton(), 56 | ], 57 | ); 58 | } 59 | } 60 | 61 | class EmailField extends ConsumerWidget { 62 | const EmailField({Key? key}) : super(key: key); 63 | 64 | @override 65 | Widget build(BuildContext context, WidgetRef ref) { 66 | final signUpState = ref.watch(signUpProvider); 67 | final showError = signUpState.email.invalid; 68 | final signUpController = ref.read(signUpProvider.notifier); 69 | return TextInputField( 70 | hintText: "Email", 71 | errorText: showError 72 | ? Email.showEmailErrorMessage(signUpState.email.error) 73 | : null, 74 | onChanged: (email) => signUpController.onEmailChange(email), 75 | ); 76 | } 77 | } 78 | 79 | class NameField extends ConsumerWidget { 80 | const NameField({Key? key}) : super(key: key); 81 | 82 | @override 83 | Widget build(BuildContext context, WidgetRef ref) { 84 | final signUpState = ref.watch(signUpProvider); 85 | final showError = signUpState.name.invalid; 86 | final signUpController = ref.read(signUpProvider.notifier); 87 | return TextInputField( 88 | hintText: "Name", 89 | errorText: 90 | showError ? Name.showNameErrorMessage(signUpState.name.error) : null, 91 | onChanged: (name) => signUpController.onNameChange(name), 92 | ); 93 | } 94 | } 95 | 96 | class PasswordField extends ConsumerWidget { 97 | const PasswordField({Key? key}) : super(key: key); 98 | 99 | @override 100 | Widget build(BuildContext context, WidgetRef ref) { 101 | final signUpState = ref.watch(signUpProvider); 102 | final showError = signUpState.password.invalid; 103 | final signUpController = ref.read(signUpProvider.notifier); 104 | return TextInputField( 105 | hintText: "Password", 106 | obscureText: true, 107 | errorText: showError 108 | ? Password.showPasswordErrorMessage(signUpState.password.error) 109 | : null, 110 | onChanged: (password) => signUpController.onPasswordChange(password), 111 | ); 112 | } 113 | } 114 | 115 | class SignUpButton extends ConsumerWidget { 116 | const SignUpButton({Key? key}) : super(key: key); 117 | 118 | @override 119 | Widget build(BuildContext context, WidgetRef ref) { 120 | final signUpState = ref.watch(signUpProvider); 121 | final signUpController = ref.read(signUpProvider.notifier); 122 | final bool isValidated = signUpState.status.isValidated; 123 | return ElevatedButton( 124 | onPressed: isValidated 125 | ? () => signUpController.signUpWithEmailAndPassword() 126 | : null, 127 | child: const Text( 128 | "Sign Up", 129 | ), 130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lib/components/auth_switch_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const _kTextStyle = TextStyle( 4 | fontWeight: FontWeight.w500, 5 | color: Colors.black54, 6 | ); 7 | 8 | class AuthSwitchButton extends StatelessWidget { 9 | final bool showSignIn; 10 | final VoidCallback onTap; 11 | const AuthSwitchButton({ 12 | Key? key, 13 | required this.showSignIn, 14 | required this.onTap, 15 | }) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Positioned( 20 | left: 0, 21 | right: 0, 22 | bottom: 30, 23 | child: TextButton( 24 | onPressed: onTap, 25 | child: showSignIn 26 | ? const Text( 27 | "Don't have account? Sign Up", 28 | style: _kTextStyle, 29 | ) 30 | : const Text( 31 | "Already have account? Sign In", 32 | style: _kTextStyle, 33 | ), 34 | ), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/components/text_input_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TextInputField extends StatelessWidget { 4 | final String hintText; 5 | final void Function(String value) onChanged; 6 | final String? errorText; 7 | final bool obscureText; 8 | 9 | const TextInputField({ 10 | Key? key, 11 | required this.hintText, 12 | required this.onChanged, 13 | this.errorText, 14 | this.obscureText = false, 15 | }) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return TextField( 20 | onChanged: onChanged, 21 | obscureText: obscureText, 22 | decoration: InputDecoration( 23 | errorText: errorText, 24 | hintText: hintText, 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'authentication/authentication_view.dart'; 4 | import 'authentication/controller/authentication_controller.dart'; 5 | import 'profile/profile.dart'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'firebase_options.dart'; 8 | 9 | void main() async { 10 | WidgetsFlutterBinding.ensureInitialized(); 11 | await Firebase.initializeApp( 12 | options: DefaultFirebaseOptions.currentPlatform, 13 | ); 14 | runApp( 15 | const ProviderScope( 16 | child: MyApp(), 17 | ), 18 | ); 19 | } 20 | 21 | class MyApp extends ConsumerWidget { 22 | const MyApp({Key? key}) : super(key: key); 23 | 24 | @override 25 | Widget build(BuildContext context, WidgetRef ref) { 26 | final authenticationState = ref.watch(authProvider); 27 | 28 | Widget getHome() { 29 | if (authenticationState.status == AuthenticationStatus.authenticated) { 30 | return const Profile(); 31 | } else if (authenticationState.status == 32 | AuthenticationStatus.unauthenticated) { 33 | return const AuthenticationView(); 34 | } else { 35 | return const AuthenticationView(); 36 | } 37 | } 38 | 39 | return MaterialApp( 40 | theme: ThemeData(), 41 | home: getHome(), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/profile/profile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import '../authentication/controller/authentication_controller.dart'; 4 | 5 | class Profile extends ConsumerWidget { 6 | const Profile({Key? key}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context, WidgetRef ref) { 10 | final authController = ref.read(authProvider.notifier); 11 | final authUser = ref.watch(authProvider).user; 12 | 13 | return Scaffold( 14 | body: Center( 15 | child: Column( 16 | mainAxisAlignment: MainAxisAlignment.center, 17 | crossAxisAlignment: CrossAxisAlignment.center, 18 | children: [ 19 | Text("user id: ${authUser.id}"), 20 | Text("user email: ${authUser.email}"), 21 | Text("email verified: ${authUser.emailVerified}"), 22 | TextButton( 23 | child: const Text("SignOut"), 24 | onPressed: () { 25 | authController.onSignOut(); 26 | }, 27 | ), 28 | ], 29 | ), 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/repository/auth_repo_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:authentication_repository/authentication_repository.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | 4 | 5 | final authRepoProvider = Provider( 6 | (_) => AuthenticationRepository(), 7 | ); -------------------------------------------------------------------------------- /packages/firebase_auth_repository/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml -------------------------------------------------------------------------------- /packages/firebase_auth_repository/lib/authentication_repository.dart: -------------------------------------------------------------------------------- 1 | export 'src/auth_user.dart'; 2 | export 'src/authentication_repository.dart'; 3 | -------------------------------------------------------------------------------- /packages/firebase_auth_repository/lib/src/auth_user.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class AuthUser extends Equatable { 4 | final String id; 5 | final String? email; 6 | final String? name; 7 | final bool emailVerified; 8 | 9 | const AuthUser({ 10 | required this.id, 11 | this.email, 12 | this.name, 13 | this.emailVerified = false, 14 | }); 15 | 16 | static const empty = AuthUser(id: ''); 17 | 18 | bool get isEmpty => this == AuthUser.empty; 19 | 20 | @override 21 | List get props => [ 22 | email, 23 | id, 24 | name, 25 | emailVerified, 26 | ]; 27 | } 28 | -------------------------------------------------------------------------------- /packages/firebase_auth_repository/lib/src/authentication_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:authentication_repository/authentication_repository.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:google_sign_in/google_sign_in.dart'; 4 | 5 | class SignUpWithEmailAndPasswordFailure implements Exception { 6 | final String code; 7 | const SignUpWithEmailAndPasswordFailure(this.code); 8 | } 9 | 10 | class SignInWithEmailAndPasswordFailure implements Exception { 11 | final String code; 12 | const SignInWithEmailAndPasswordFailure(this.code); 13 | } 14 | 15 | class ForgotPasswordFailure implements Exception { 16 | final String code; 17 | const ForgotPasswordFailure(this.code); 18 | } 19 | 20 | class SignInWithGoogleFailure implements Exception {} 21 | 22 | class SignOutFailure implements Exception {} 23 | 24 | class AuthenticationRepository { 25 | final _firebaseAuth = FirebaseAuth.instance; 26 | final _googleSignIn = GoogleSignIn.standard(); 27 | 28 | Stream get user { 29 | return _firebaseAuth.authStateChanges().map((firebaseUser) { 30 | return firebaseUser == null 31 | ? AuthUser.empty 32 | : AuthUser( 33 | id: firebaseUser.uid, 34 | email: firebaseUser.email, 35 | name: firebaseUser.displayName, 36 | emailVerified: firebaseUser.emailVerified, 37 | ); 38 | }); 39 | } 40 | 41 | Future signUpWithEmailAndPassword( 42 | {required String email, required String password}) async { 43 | try { 44 | await _firebaseAuth.createUserWithEmailAndPassword( 45 | email: email, 46 | password: password, 47 | ); 48 | } on FirebaseAuthException catch (e) { 49 | throw SignUpWithEmailAndPasswordFailure(e.code); 50 | } 51 | } 52 | 53 | Future signInWithEmailAndPassword({ 54 | required String email, 55 | required String password, 56 | }) async { 57 | try { 58 | await _firebaseAuth.signInWithEmailAndPassword( 59 | email: email, 60 | password: password, 61 | ); 62 | } on FirebaseAuthException catch (e) { 63 | throw SignInWithEmailAndPasswordFailure(e.code); 64 | } 65 | } 66 | 67 | Future forgotPassword({required String email}) async { 68 | try { 69 | await _firebaseAuth.sendPasswordResetEmail(email: email); 70 | } on FirebaseAuthException catch (e) { 71 | throw ForgotPasswordFailure(e.code); 72 | } 73 | } 74 | 75 | Future signInWithGoogle() async { 76 | try { 77 | final googleSignInAccount = await _googleSignIn.signIn(); 78 | if (googleSignInAccount == null) { 79 | throw SignInWithGoogleFailure(); 80 | } 81 | 82 | final googleSignInAuth = await googleSignInAccount.authentication; 83 | 84 | final credential = GoogleAuthProvider.credential( 85 | accessToken: googleSignInAuth.accessToken, 86 | idToken: googleSignInAuth.idToken, 87 | ); 88 | 89 | final userCredential = 90 | await _firebaseAuth.signInWithCredential(credential); 91 | 92 | return userCredential.additionalUserInfo?.isNewUser; 93 | } on FirebaseAuthException catch (_) { 94 | throw SignInWithGoogleFailure(); 95 | } 96 | } 97 | 98 | Future signOut() async { 99 | try { 100 | await Future.wait([ 101 | _firebaseAuth.signOut(), 102 | _googleSignIn.signOut(), 103 | ]); 104 | } catch (_) { 105 | throw SignOutFailure(); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /packages/firebase_auth_repository/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: authentication_repository 2 | 3 | 4 | environment: 5 | sdk: ">=2.16.0 <3.0.0" 6 | 7 | dev_dependencies: 8 | flutter_lints: ^1.0.4 9 | 10 | dependencies: 11 | equatable: ^2.0.3 12 | firebase_auth: ^3.3.18 13 | google_sign_in: ^5.3.2 14 | -------------------------------------------------------------------------------- /packages/form_validators/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml -------------------------------------------------------------------------------- /packages/form_validators/lib/form_validators.dart: -------------------------------------------------------------------------------- 1 | export 'src/name.dart'; 2 | export 'src/email.dart'; 3 | export 'src/password.dart'; 4 | export 'package:formz/formz.dart'; -------------------------------------------------------------------------------- /packages/form_validators/lib/src/email.dart: -------------------------------------------------------------------------------- 1 | import 'package:form_validators/form_validators.dart'; 2 | 3 | enum EmailValidationError { empty, invalid } 4 | 5 | const String _kEmailPattern = 6 | r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$'; 7 | 8 | class Email extends FormzInput { 9 | const Email.pure() : super.pure(''); 10 | const Email.dirty([String value = '']) : super.dirty(value); 11 | 12 | static final _regex = RegExp(_kEmailPattern); 13 | 14 | @override 15 | EmailValidationError? validator(String value) { 16 | if (_regex.hasMatch(value)) { 17 | return null; 18 | } else if (value.isEmpty) { 19 | return EmailValidationError.empty; 20 | } else { 21 | return EmailValidationError.invalid; 22 | } 23 | } 24 | 25 | static String? showEmailErrorMessage(EmailValidationError? error) { 26 | if (error == EmailValidationError.empty) { 27 | return 'Empty email'; 28 | } else if (error == EmailValidationError.invalid) { 29 | return 'Invalid email'; 30 | } else { 31 | return null; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/form_validators/lib/src/name.dart: -------------------------------------------------------------------------------- 1 | import 'package:formz/formz.dart'; 2 | 3 | enum NameValidationError { empty, invalid } 4 | 5 | class Name extends FormzInput { 6 | const Name.pure() : super.pure(''); 7 | const Name.dirty([String value = '']) : super.dirty(value); 8 | 9 | @override 10 | NameValidationError? validator(String value) { 11 | if (value.isEmpty) { 12 | return NameValidationError.empty; 13 | } else if (value.length < 3) { 14 | return NameValidationError.invalid; 15 | } else { 16 | return null; 17 | } 18 | } 19 | 20 | static String? showNameErrorMessage(NameValidationError? error) { 21 | if (error == NameValidationError.empty) { 22 | return 'Empty name'; 23 | } else if (error == NameValidationError.invalid) { 24 | return 'Too short name'; 25 | } else { 26 | return null; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/form_validators/lib/src/password.dart: -------------------------------------------------------------------------------- 1 | import 'package:formz/formz.dart'; 2 | 3 | enum PasswordValidationError { empty, invalid } 4 | 5 | class Password extends FormzInput { 6 | const Password.pure() : super.pure(''); 7 | const Password.dirty([String value = '']) : super.dirty(value); 8 | 9 | @override 10 | PasswordValidationError? validator(String value) { 11 | if (value.isEmpty) { 12 | return PasswordValidationError.empty; 13 | } else if (value.length < 6) { 14 | return PasswordValidationError.invalid; 15 | } else { 16 | return null; 17 | } 18 | } 19 | 20 | static String? showPasswordErrorMessage(PasswordValidationError? error) { 21 | if (error == PasswordValidationError.empty) { 22 | return 'Empty password'; 23 | } else if (error == PasswordValidationError.invalid) { 24 | return 'Invalid password'; 25 | } else { 26 | return null; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/form_validators/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: form_validators 2 | 3 | 4 | environment: 5 | sdk: ">=2.16.0 <3.0.0" 6 | 7 | dev_dependencies: 8 | flutter_lints: ^1.0.4 9 | 10 | dependencies: 11 | formz: ^0.4.1 -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: firebase_authentication_riverpod 2 | description: A new Flutter project. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.17.0 <3.0.0" 22 | 23 | # Dependencies specify other packages that your package needs in order to work. 24 | # To automatically upgrade your package dependencies to the latest versions 25 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 26 | # dependencies can be manually updated by changing the version numbers below to 27 | # the latest version available on pub.dev. To see which dependencies have newer 28 | # versions available, run `flutter pub outdated`. 29 | dependencies: 30 | flutter: 31 | sdk: flutter 32 | 33 | 34 | # The following adds the Cupertino Icons font to your application. 35 | # Use with the CupertinoIcons class for iOS style icons. 36 | cupertino_icons: ^1.0.2 37 | flutter_riverpod: 2.0.0-dev.9 38 | equatable: ^2.0.3 39 | form_validators: 40 | path: packages/form_validators 41 | authentication_repository: 42 | path: packages/firebase_auth_repository 43 | firebase_core: ^1.17.0 44 | dev_dependencies: 45 | flutter_test: 46 | sdk: flutter 47 | 48 | # The "flutter_lints" package below contains a set of recommended lints to 49 | # encourage good coding practices. The lint set provided by the package is 50 | # activated in the `analysis_options.yaml` file located at the root of your 51 | # package. See that file for information about deactivating specific lint 52 | # rules and activating additional ones. 53 | flutter_lints: ^2.0.0 54 | 55 | # For information on the generic Dart part of this file, see the 56 | # following page: https://dart.dev/tools/pub/pubspec 57 | 58 | # The following section is specific to Flutter packages. 59 | flutter: 60 | 61 | # The following line ensures that the Material Icons font is 62 | # included with your application, so that you can use the icons in 63 | # the material Icons class. 64 | uses-material-design: true 65 | 66 | # To add assets to your application, add an assets section, like this: 67 | # assets: 68 | # - images/a_dot_burr.jpeg 69 | # - images/a_dot_ham.jpeg 70 | 71 | # An image asset can refer to one or more resolution-specific "variants", see 72 | # https://flutter.dev/assets-and-images/#resolution-aware 73 | 74 | # For details regarding adding assets from package dependencies, see 75 | # https://flutter.dev/assets-and-images/#from-packages 76 | 77 | # To add custom fonts to your application, add a fonts section here, 78 | # in this "flutter" section. Each entry in this list should have a 79 | # "family" key with the font family name, and a "fonts" key with a 80 | # list giving the asset and other descriptors for the font. For 81 | # example: 82 | # fonts: 83 | # - family: Schyler 84 | # fonts: 85 | # - asset: fonts/Schyler-Regular.ttf 86 | # - asset: fonts/Schyler-Italic.ttf 87 | # style: italic 88 | # - family: Trajan Pro 89 | # fonts: 90 | # - asset: fonts/TrajanPro.ttf 91 | # - asset: fonts/TrajanPro_Bold.ttf 92 | # weight: 700 93 | # 94 | # For details regarding fonts from package dependencies, 95 | # see https://flutter.dev/custom-fonts/#from-packages 96 | --------------------------------------------------------------------------------