├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── flutter_tdd_and_clean_architecture │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── RunnerTests │ └── RunnerTests.swift └── firebase_app_id_file.json ├── lib ├── main.dart └── src │ └── features │ └── auth │ ├── data │ ├── data_sources │ │ ├── auth_local_data_source.dart │ │ ├── auth_remote_data_source.dart │ │ ├── auth_remote_data_source_fake.dart │ │ └── auth_remote_data_source_firebase.dart │ ├── models │ │ └── auth_user_model.dart │ └── repositories │ │ └── auth_repository_impl.dart │ ├── domain │ ├── entities │ │ └── auth_user.dart │ ├── repositories │ │ └── auth_repository.dart │ ├── use_cases │ │ ├── sign_in_use_case.dart │ │ ├── sign_out_use_case.dart │ │ ├── sign_up_use_case.dart │ │ └── stream_auth_user_use_case.dart │ └── value_objects │ │ ├── email.dart │ │ ├── email.g.dart │ │ ├── password.dart │ │ └── password.g.dart │ └── presentation │ ├── blocs │ ├── email_status.dart │ ├── form_status.dart │ ├── password_status.dart │ ├── sign_in │ │ ├── sign_in_cubit.dart │ │ └── sign_in_state.dart │ └── sign_up │ │ ├── sign_up_cubit.dart │ │ └── sign_up_state.dart │ └── screens │ ├── sign_in_screen.dart │ └── sign_up_screen.dart ├── pubspec.lock ├── pubspec.yaml ├── screenshots └── clean_architecture_course.png ├── test └── src │ └── features │ └── auth │ ├── data │ ├── data_sources │ │ ├── auth_local_data_source_test.dart │ │ ├── auth_remote_data_source_firebase_test.dart │ │ └── auth_remote_data_source_firebase_test.mocks.dart │ ├── models │ │ ├── auth_user_model_test.dart │ │ └── auth_user_model_test.mocks.dart │ └── repositories │ │ ├── auth_repository_impl_test.dart │ │ └── auth_repository_impl_test.mocks.dart │ ├── domain │ ├── entities │ │ └── auth_user_test.dart │ ├── use_cases │ │ ├── sign_in_use_case_test.dart │ │ ├── sign_in_use_case_test.mocks.dart │ │ ├── sign_out_use_case_test.dart │ │ ├── sign_out_use_case_test.mocks.dart │ │ ├── sign_up_use_case_test.dart │ │ ├── sign_up_use_case_test.mocks.dart │ │ ├── stream_auth_user_use_case_test.dart │ │ └── stream_auth_user_use_case_test.mocks.dart │ └── value_objects │ │ ├── email_test.dart │ │ └── password_test.dart │ └── presentation │ ├── blocs │ ├── sign_in │ │ ├── sign_in_cubit_test.dart │ │ ├── sign_in_cubit_test.mocks.dart │ │ └── sign_in_state_test.dart │ └── sign_up │ │ ├── sign_up_cubit_test.dart │ │ ├── sign_up_cubit_test.mocks.dart │ │ └── sign_up_state_test.dart │ └── screens │ ├── sign_in_screen_test.dart │ ├── sign_in_screen_test.mocks.dart │ ├── sign_up_screen_test.dart │ └── sign_up_screen_test.mocks.dart └── web ├── favicon.png ├── icons ├── Icon-192.png ├── Icon-512.png ├── Icon-maskable-192.png └── Icon-maskable-512.png ├── index.html └── manifest.json /.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 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | -------------------------------------------------------------------------------- /.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: 84a1e904f44f9b0e9c4510138010edcc653163f8 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: 84a1e904f44f9b0e9c4510138010edcc653163f8 17 | base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 18 | - platform: android 19 | create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 20 | base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 21 | - platform: ios 22 | create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 23 | base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 24 | - platform: linux 25 | create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 26 | base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 27 | - platform: macos 28 | create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 29 | base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 30 | - platform: web 31 | create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 32 | base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 33 | - platform: windows 34 | create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 35 | base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Clean Architecture with Firebase 2 | 3 | This project demonstrates the use of advanced topics such as Clean Architecture, Firebase integration, Bloc pattern for state management, and Hive for local storage. 4 | 5 | ## Structure 6 | 7 | The application follows the principles of Clean Architecture and is structured into three primary layers: 8 | 9 | - **Data Layer**: This layer includes the models, data sources (remote and local), and the repositories. It interacts with Firebase for remote data and Hive for local data. 10 | 11 | - **Domain Layer**: This layer contains the core business logic (use cases) and entity definitions. 12 | 13 | - **Presentation Layer**: This is where the UI-related logic resides. It uses Bloc for state management. 14 | 15 | The application has three main features/modules: 16 | 17 | - **Auth**: Deals with user authentication, both locally and remotely through Firebase. 18 | 19 | - **Feed** (not included in the preview): Handles the creation, fetching, and management of user posts. 20 | 21 | - **Chat** (not included in the preview): Manages real-time chat functionality between users. 22 | 23 | All modules follow the above-mentioned architectural layers and are organized accordingly within their respective directories. 24 | 25 | ## Usage 26 | 27 | - **User Authentication**: User can register, login and logout. 28 | 29 | - **Feed Posting**: Users can view posts. Coming soon: add and delete posts. 30 | 31 | - **Chat**: Users can chat with other users in real-time. 32 | 33 | Please note that the code is written in a way to be easily read, extended, and maintained. This is in line with the principles of Clean Architecture, which aims to separate concerns and make the codebase more understandable and flexible. 34 | 35 | ## Customization 36 | 37 | Feel free to customize the code to suit your needs. The codebase is modular and flexible, making it easy to add, remove, or modify features. 38 | 39 | ## Dependencies 40 | 41 | The application is built using various dependencies that aid in implementing its features: 42 | 43 | - Firebase Core, Firestore, and Auth for backend operations. 44 | - Hive for local database operations. 45 | - Bloc for state management. 46 | - Equatable, UUID, Built Value for data handling. 47 | - Google Fonts and Flex Color Scheme for UI styling. 48 | - Functional error handling with fpdart (not included in the preview) 49 | - And several other for testing and mocking. 50 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | // START: FlutterFire Configuration 26 | apply plugin: 'com.google.gms.google-services' 27 | // END: FlutterFire Configuration 28 | apply plugin: 'kotlin-android' 29 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 30 | 31 | android { 32 | namespace "com.example.flutter_clean_architecture_with_firebase" 33 | compileSdkVersion flutter.compileSdkVersion 34 | ndkVersion flutter.ndkVersion 35 | 36 | compileOptions { 37 | sourceCompatibility JavaVersion.VERSION_1_8 38 | targetCompatibility JavaVersion.VERSION_1_8 39 | } 40 | 41 | kotlinOptions { 42 | jvmTarget = '1.8' 43 | } 44 | 45 | sourceSets { 46 | main.java.srcDirs += 'src/main/kotlin' 47 | } 48 | 49 | defaultConfig { 50 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 51 | applicationId "com.example.flutter_clean_architecture_with_firebase" 52 | // You can update the following values to match your application needs. 53 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 54 | minSdkVersion flutter.minSdkVersion 55 | targetSdkVersion flutter.targetSdkVersion 56 | versionCode flutterVersionCode.toInteger() 57 | versionName flutterVersionName 58 | } 59 | 60 | buildTypes { 61 | release { 62 | // TODO: Add your own signing config for the release build. 63 | // Signing with the debug keys for now, so `flutter run --release` works. 64 | signingConfig signingConfigs.debug 65 | } 66 | } 67 | } 68 | 69 | flutter { 70 | source '../..' 71 | } 72 | 73 | dependencies { 74 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 75 | } 76 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/flutter_tdd_and_clean_architecture/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.flutter_clean_architecture_with_firebase 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.3.0' 10 | // START: FlutterFire Configuration 11 | classpath 'com.google.gms:google-services:4.3.10' 12 | // END: FlutterFire Configuration 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | rootProject.buildDir = '../build' 25 | subprojects { 26 | project.buildDir = "${rootProject.buildDir}/${project.name}" 27 | } 28 | subprojects { 29 | project.evaluationDependsOn(':app') 30 | } 31 | 32 | tasks.register("clean", Delete) { 33 | delete rootProject.buildDir 34 | } 35 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Flutter Tdd And Clean Architecture 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | flutter_clean_architecture_with_firebase 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /ios/firebase_app_id_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "file_generated_by": "FlutterFire CLI", 3 | "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", 4 | "GOOGLE_APP_ID": "1:386454341522:ios:c51154e06897c00796234b", 5 | "FIREBASE_PROJECT_ID": "flutter-tdd-clean-architecture", 6 | "GCM_SENDER_ID": "386454341522" 7 | } -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:firebase_core/firebase_core.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | 7 | import 'firebase_options.dart'; 8 | import 'src/features/auth/data/data_sources/auth_local_data_source.dart'; 9 | import 'src/features/auth/data/data_sources/auth_remote_data_source.dart'; 10 | import 'src/features/auth/data/data_sources/auth_remote_data_source_firebase.dart'; 11 | import 'src/features/auth/data/repositories/auth_repository_impl.dart'; 12 | import 'src/features/auth/domain/entities/auth_user.dart'; 13 | import 'src/features/auth/domain/repositories/auth_repository.dart'; 14 | import 'src/features/auth/presentation/screens/sign_in_screen.dart'; 15 | 16 | typedef AppBuilder = Future Function(); 17 | 18 | Future bootstrap(AppBuilder builder) async { 19 | WidgetsFlutterBinding.ensureInitialized(); 20 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 21 | runApp(await builder()); 22 | } 23 | 24 | void main() { 25 | bootstrap( 26 | () async { 27 | AuthLocalDataSource authLocalDataSource = AuthLocalDataSource(); 28 | AuthRemoteDataSource authRemoteDataSource = 29 | AuthRemoteDataSourceFirebase(); 30 | 31 | AuthRepository authRepository = AuthRepositoryImpl( 32 | localDataSource: authLocalDataSource, 33 | remoteDataSource: authRemoteDataSource, 34 | ); 35 | 36 | return App( 37 | authRepository: authRepository, 38 | authUser: await authRepository.authUser.first, 39 | ); 40 | }, 41 | ); 42 | } 43 | 44 | class App extends StatelessWidget { 45 | const App({ 46 | super.key, 47 | required this.authRepository, 48 | this.authUser, 49 | }); 50 | 51 | final AuthRepository authRepository; 52 | final AuthUser? authUser; 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | return MultiRepositoryProvider( 57 | providers: [ 58 | RepositoryProvider.value(value: authRepository), 59 | ], 60 | child: MaterialApp( 61 | title: 'Clean Architecture', 62 | theme: ThemeData.light(useMaterial3: true), 63 | home: const SignInScreen(), 64 | ), 65 | ); 66 | } 67 | } 68 | 69 | // class HomeScreen extends StatelessWidget { 70 | // const HomeScreen({super.key}); 71 | 72 | // @override 73 | // Widget build(BuildContext context) { 74 | // return Scaffold( 75 | // appBar: AppBar(title: const Text('Clean Architecture')), 76 | // body: const Column(children: []), 77 | // ); 78 | // } 79 | // } 80 | -------------------------------------------------------------------------------- /lib/src/features/auth/data/data_sources/auth_local_data_source.dart: -------------------------------------------------------------------------------- 1 | class AuthLocalDataSource { 2 | AuthLocalDataSource() : authLocalDataSource = {}; 3 | 4 | final Map authLocalDataSource; 5 | 6 | void write({required String key, T? value}) { 7 | authLocalDataSource[key] = value; 8 | } 9 | 10 | T? read({required String key}) { 11 | final value = authLocalDataSource[key]; 12 | if (value is T) return value; 13 | return null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/features/auth/data/data_sources/auth_remote_data_source.dart: -------------------------------------------------------------------------------- 1 | import '../models/auth_user_model.dart'; 2 | 3 | abstract class AuthRemoteDataSource { 4 | Stream get user; 5 | 6 | Future signUpWithEmailAndPassword({ 7 | required String email, 8 | required String password, 9 | }); 10 | 11 | Future signInWithEmailAndPassword({ 12 | required String email, 13 | required String password, 14 | }); 15 | 16 | Future signOut(); 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/features/auth/data/data_sources/auth_remote_data_source_fake.dart: -------------------------------------------------------------------------------- 1 | import '../models/auth_user_model.dart'; 2 | import 'auth_remote_data_source.dart'; 3 | 4 | class AuthRemoteDataSourceFake implements AuthRemoteDataSource { 5 | static const fakeUser = AuthUserModel( 6 | id: 'fake-user-id', 7 | email: 'fake-user-email', 8 | name: 'fake-user-name', 9 | ); 10 | 11 | @override 12 | Stream get user { 13 | return Stream.value(fakeUser); 14 | } 15 | 16 | @override 17 | Future signUpWithEmailAndPassword({ 18 | required String email, 19 | required String password, 20 | }) async { 21 | await Future.delayed(const Duration(seconds: 1)); 22 | return fakeUser; 23 | } 24 | 25 | @override 26 | Future signInWithEmailAndPassword({ 27 | required String email, 28 | required String password, 29 | }) async { 30 | await Future.delayed(const Duration(seconds: 1)); 31 | return fakeUser; 32 | } 33 | 34 | @override 35 | Future signOut() async { 36 | throw UnimplementedError(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/features/auth/data/data_sources/auth_remote_data_source_firebase.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; 2 | 3 | import '../models/auth_user_model.dart'; 4 | import 'auth_remote_data_source.dart'; 5 | 6 | class AuthRemoteDataSourceFirebase implements AuthRemoteDataSource { 7 | AuthRemoteDataSourceFirebase({ 8 | firebase_auth.FirebaseAuth? firebaseAuth, 9 | }) : _firebaseAuth = firebaseAuth ?? firebase_auth.FirebaseAuth.instance; 10 | 11 | final firebase_auth.FirebaseAuth _firebaseAuth; 12 | 13 | @override 14 | Stream get user { 15 | return _firebaseAuth.authStateChanges().map((firebaseUser) { 16 | if (firebaseUser == null) { 17 | return null; 18 | } 19 | return AuthUserModel.fromFirebaseAuthUser(firebaseUser); 20 | }); 21 | } 22 | 23 | @override 24 | Future signUpWithEmailAndPassword({ 25 | required String email, 26 | required String password, 27 | }) async { 28 | try { 29 | firebase_auth.UserCredential credential = 30 | await _firebaseAuth.createUserWithEmailAndPassword( 31 | email: email, 32 | password: password, 33 | ); 34 | 35 | if (credential.user == null) { 36 | throw Exception('Sign up failed: The user is null after sign up.'); 37 | } 38 | 39 | return AuthUserModel.fromFirebaseAuthUser(credential.user!); 40 | } catch (error) { 41 | throw Exception('Sign up failed: $error'); 42 | } 43 | } 44 | 45 | @override 46 | Future signInWithEmailAndPassword({ 47 | required String email, 48 | required String password, 49 | }) async { 50 | try { 51 | firebase_auth.UserCredential credential = 52 | await _firebaseAuth.signInWithEmailAndPassword( 53 | email: email, 54 | password: password, 55 | ); 56 | 57 | if (credential.user == null) { 58 | throw Exception('Sign in failed: The user is null after sign in.'); 59 | } 60 | 61 | return AuthUserModel.fromFirebaseAuthUser(credential.user!); 62 | } catch (error) { 63 | throw Exception('Sign in failed: $error'); 64 | } 65 | } 66 | 67 | @override 68 | Future signOut() async { 69 | try { 70 | await _firebaseAuth.signOut(); 71 | } catch (error) { 72 | throw Exception('Sign out failed: $error'); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/src/features/auth/data/models/auth_user_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; 3 | 4 | import '../../domain/entities/auth_user.dart'; 5 | 6 | class AuthUserModel extends Equatable { 7 | final String id; 8 | final String email; 9 | final String? name; 10 | final String? photoURL; 11 | 12 | const AuthUserModel({ 13 | required this.id, 14 | required this.email, 15 | this.name, 16 | this.photoURL, 17 | }); 18 | 19 | factory AuthUserModel.fromFirebaseAuthUser( 20 | firebase_auth.User firebaseUser, 21 | ) { 22 | return AuthUserModel( 23 | id: firebaseUser.uid, 24 | email: firebaseUser.email ?? '', 25 | name: firebaseUser.displayName, 26 | photoURL: firebaseUser.photoURL, 27 | ); 28 | } 29 | 30 | // factory AuthUserModel.fromAnotherDataSourceAuthUser( 31 | // // ... 32 | // ) { 33 | // return AuthUserModel( 34 | // ... 35 | // ); 36 | // } 37 | 38 | AuthUser toEntity() { 39 | return AuthUser( 40 | id: id, 41 | email: email, 42 | name: name, 43 | photoURL: photoURL, 44 | ); 45 | } 46 | 47 | @override 48 | List get props => [id, email, name, photoURL]; 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/features/auth/data/repositories/auth_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import '../../domain/entities/auth_user.dart'; 2 | import '../../domain/repositories/auth_repository.dart'; 3 | import '../data_sources/auth_local_data_source.dart'; 4 | import '../data_sources/auth_remote_data_source.dart'; 5 | 6 | class AuthRepositoryImpl implements AuthRepository { 7 | final AuthRemoteDataSource remoteDataSource; 8 | final AuthLocalDataSource localDataSource; 9 | 10 | const AuthRepositoryImpl({ 11 | required this.remoteDataSource, 12 | required this.localDataSource, 13 | }); 14 | 15 | @override 16 | Stream get authUser { 17 | return remoteDataSource.user.map((authUserModel) { 18 | if (authUserModel != null) { 19 | localDataSource.write(key: 'user', value: authUserModel); 20 | } else { 21 | localDataSource.write(key: 'user', value: null); 22 | } 23 | 24 | return authUserModel == null ? AuthUser.empty : authUserModel.toEntity(); 25 | }); 26 | } 27 | 28 | @override 29 | Future signUp({ 30 | required String email, 31 | required String password, 32 | }) async { 33 | final authModel = await remoteDataSource.signUpWithEmailAndPassword( 34 | email: email, 35 | password: password, 36 | ); 37 | 38 | localDataSource.write(key: 'user', value: authModel); 39 | 40 | return authModel.toEntity(); 41 | } 42 | 43 | @override 44 | Future signIn({ 45 | required String email, 46 | required String password, 47 | }) async { 48 | final authModel = await remoteDataSource.signInWithEmailAndPassword( 49 | email: email, 50 | password: password, 51 | ); 52 | 53 | localDataSource.write(key: 'user', value: authModel); 54 | 55 | return authModel.toEntity(); 56 | } 57 | 58 | @override 59 | Future signOut() async { 60 | await remoteDataSource.signOut(); 61 | 62 | localDataSource.write(key: 'user', value: null); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/features/auth/domain/entities/auth_user.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | // They are the business objects of the application (Enterprise-wide business rules) 4 | // and encapsulate the most general and high-level rules. 5 | class AuthUser extends Equatable { 6 | final String id; 7 | final String email; 8 | final String? name; 9 | final String? photoURL; 10 | // For example, a high-level rule for a User entity might be 11 | // that a user age cannot be lower than 18. 12 | // final int age; 13 | 14 | const AuthUser({ 15 | required this.id, 16 | required this.email, 17 | this.name, 18 | this.photoURL, 19 | }); 20 | 21 | static const AuthUser empty = AuthUser( 22 | id: '', 23 | name: '', 24 | email: '', 25 | photoURL: '', 26 | ); 27 | 28 | // An entity can be an object with methods, or it can be a set of 29 | // data structures and functions. 30 | bool get isEmpty => this == AuthUser.empty; 31 | 32 | @override 33 | List get props => [id, name, email, photoURL]; 34 | 35 | // DO NOT ADD THIS CONSTRUCTOR: 36 | // It's not the role of the entity to know how to serialize 37 | // and deserialize data from external data source. 38 | // factory AuthUser.fromJson(Map json) { 39 | // return AuthUser( 40 | // id: json['id'] as String, 41 | // name: json['name'] as String, 42 | // email: json['email'] as String, 43 | // photoURL: json['photoURL'] as String, 44 | // ); 45 | // } 46 | } 47 | 48 | 49 | 50 | 51 | 52 | // They are plain Dart (or whatever language you're using) classes and don't have 53 | // any dependencies on other layers. 54 | 55 | 56 | // In the context of Clean Architecture, "business objects" refer to the main components 57 | // or objects that the application is built around. These are usually things that 58 | // represent real-world objects or concepts that are relevant to what the 59 | // application does. For example, in a banking app, some of the business objects 60 | // might be Account, Transaction, or Customer. 61 | 62 | // These rules are the basic principles or guidelines that define how the 63 | // entities behave or interact.This rule would be "encapsulated" in the Account entity, meaning that 64 | // it's a fundamental part of what defines an Account in 65 | // the context of the application. -------------------------------------------------------------------------------- /lib/src/features/auth/domain/repositories/auth_repository.dart: -------------------------------------------------------------------------------- 1 | import '../entities/auth_user.dart'; 2 | 3 | // Think of the Repository as a tool that you can perform 4 | // CRUD operations (Create, Read, Update, Delete) on. 5 | // Repositories in the domain layer do not specify how the 6 | // data is actually stored or retrieved, they just define 7 | // what you can do with the data. 8 | abstract class AuthRepository { 9 | // Single Responsibility: Each Repository has a single responsibility, 10 | // which is to provide data for a specific type of entity. 11 | 12 | Stream get authUser; 13 | 14 | Future signUp({ 15 | required String email, 16 | required String password, 17 | }); 18 | 19 | Future signIn({ 20 | required String email, 21 | required String password, 22 | }); 23 | 24 | Future signOut(); 25 | } 26 | 27 | // This is crucial for the Clean Architecture's goal of separation 28 | // of concerns and making the system more flexible and adaptable. 29 | // If you want to change how the data is stored - say, moving 30 | // from a relational database to a NoSQL database, or even 31 | // to an in-memory database for testing - you can do so 32 | // by implementing a new Repository that adheres to the same 33 | // interface, without having to change your application's core logic. 34 | 35 | // The main role of a Repository is to hide the details of how data 36 | // for the entities is fetched or stored. It provides a clean API 37 | // for the rest of the application to obtain domain entities without needing to know 38 | // where they come from or what type of infrastructure is used to store them. 39 | -------------------------------------------------------------------------------- /lib/src/features/auth/domain/use_cases/sign_in_use_case.dart: -------------------------------------------------------------------------------- 1 | import '../entities/auth_user.dart'; 2 | import '../repositories/auth_repository.dart'; 3 | import '../value_objects/email.dart'; 4 | import '../value_objects/password.dart'; 5 | 6 | // Each Use Case should have a single responsibility. It should represent 7 | // one and only one action that a user can perform. 8 | class SignInUseCase { 9 | // Use cases don't know anything about the underlying data sources. 10 | final AuthRepository authRepository; 11 | 12 | SignInUseCase({required this.authRepository}); 13 | 14 | // The primary role of a use case is to orchestrate the execution of 15 | // a specific business operation. They coordinate the flow of data 16 | // to and from entities by interacting with repositories. 17 | Future call(SignInParams params) async { 18 | try { 19 | return await authRepository.signIn( 20 | email: params.email.value, 21 | password: params.password.value, 22 | ); 23 | } on ArgumentError catch (error) { 24 | throw Exception(error); 25 | } catch (error) { 26 | throw Exception(error); 27 | } 28 | } 29 | } 30 | 31 | // You can bundle several parameters into one object 32 | // that can be easily passed around. 33 | class SignInParams { 34 | final Email email; 35 | final Password password; 36 | 37 | SignInParams({ 38 | required this.email, 39 | required this.password, 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/features/auth/domain/use_cases/sign_out_use_case.dart: -------------------------------------------------------------------------------- 1 | import '../repositories/auth_repository.dart'; 2 | 3 | class SignOutUseCase { 4 | final AuthRepository authRepository; 5 | 6 | SignOutUseCase({required this.authRepository}); 7 | 8 | Future call() async { 9 | try { 10 | await authRepository.signOut(); 11 | } catch (error) { 12 | throw Exception(error); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/features/auth/domain/use_cases/sign_up_use_case.dart: -------------------------------------------------------------------------------- 1 | import '../entities/auth_user.dart'; 2 | import '../repositories/auth_repository.dart'; 3 | import '../value_objects/email.dart'; 4 | import '../value_objects/password.dart'; 5 | 6 | class SignUpUseCase { 7 | final AuthRepository authRepository; 8 | 9 | SignUpUseCase({required this.authRepository}); 10 | 11 | Future call(SignUpParams params) async { 12 | try { 13 | AuthUser authUser = await authRepository.signUp( 14 | email: params.email.value, 15 | password: params.password.value, 16 | ); 17 | return authUser; 18 | } on ArgumentError catch (error) { 19 | throw Exception(error); 20 | } catch (error) { 21 | throw Exception(error); 22 | } 23 | } 24 | } 25 | 26 | class SignUpParams { 27 | final Email email; 28 | final Password password; 29 | 30 | SignUpParams({ 31 | required this.email, 32 | required this.password, 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/features/auth/domain/use_cases/stream_auth_user_use_case.dart: -------------------------------------------------------------------------------- 1 | import '../entities/auth_user.dart'; 2 | import '../repositories/auth_repository.dart'; 3 | 4 | class StreamAuthUserUseCase { 5 | final AuthRepository authRepository; 6 | 7 | StreamAuthUserUseCase({required this.authRepository}); 8 | 9 | Stream call() { 10 | try { 11 | return authRepository.authUser; 12 | } catch (error) { 13 | throw Exception(error); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/features/auth/domain/value_objects/email.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | 3 | part 'email.g.dart'; 4 | 5 | // Value object are defined by their attributes rather than its identity. 6 | abstract class Email implements Built { 7 | String get value; 8 | 9 | // Entities tend to represent more complex objects or concepts in 10 | // your domain, while value objects are used to encapsulate simpler, 11 | // more atomic concepts or measures. Both play important roles 12 | // in creating a rich, expressive domain model. 13 | Email._() { 14 | final RegExp emailRegExp = RegExp( 15 | r'^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+', 16 | ); 17 | 18 | if (!emailRegExp.hasMatch(value)) { 19 | throw ArgumentError('Invalid email format'); 20 | } 21 | } 22 | factory Email([void Function(EmailBuilder) updates]) = _$Email; 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/features/auth/domain/value_objects/email.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'email.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | class _$Email extends Email { 10 | @override 11 | final String value; 12 | 13 | factory _$Email([void Function(EmailBuilder)? updates]) => 14 | (new EmailBuilder()..update(updates))._build(); 15 | 16 | _$Email._({required this.value}) : super._() { 17 | BuiltValueNullFieldError.checkNotNull(value, r'Email', 'value'); 18 | } 19 | 20 | @override 21 | Email rebuild(void Function(EmailBuilder) updates) => 22 | (toBuilder()..update(updates)).build(); 23 | 24 | @override 25 | EmailBuilder toBuilder() => new EmailBuilder()..replace(this); 26 | 27 | @override 28 | bool operator ==(Object other) { 29 | if (identical(other, this)) return true; 30 | return other is Email && value == other.value; 31 | } 32 | 33 | @override 34 | int get hashCode { 35 | var _$hash = 0; 36 | _$hash = $jc(_$hash, value.hashCode); 37 | _$hash = $jf(_$hash); 38 | return _$hash; 39 | } 40 | 41 | @override 42 | String toString() { 43 | return (newBuiltValueToStringHelper(r'Email')..add('value', value)) 44 | .toString(); 45 | } 46 | } 47 | 48 | class EmailBuilder implements Builder { 49 | _$Email? _$v; 50 | 51 | String? _value; 52 | String? get value => _$this._value; 53 | set value(String? value) => _$this._value = value; 54 | 55 | EmailBuilder(); 56 | 57 | EmailBuilder get _$this { 58 | final $v = _$v; 59 | if ($v != null) { 60 | _value = $v.value; 61 | _$v = null; 62 | } 63 | return this; 64 | } 65 | 66 | @override 67 | void replace(Email other) { 68 | ArgumentError.checkNotNull(other, 'other'); 69 | _$v = other as _$Email; 70 | } 71 | 72 | @override 73 | void update(void Function(EmailBuilder)? updates) { 74 | if (updates != null) updates(this); 75 | } 76 | 77 | @override 78 | Email build() => _build(); 79 | 80 | _$Email _build() { 81 | final _$result = _$v ?? 82 | new _$Email._( 83 | value: BuiltValueNullFieldError.checkNotNull( 84 | value, r'Email', 'value')); 85 | replace(_$result); 86 | return _$result; 87 | } 88 | } 89 | 90 | // ignore_for_file: deprecated_member_use_from_same_package,type=lint 91 | -------------------------------------------------------------------------------- /lib/src/features/auth/domain/value_objects/password.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | 3 | part 'password.g.dart'; 4 | 5 | abstract class Password implements Built { 6 | String get value; 7 | 8 | Password._() { 9 | if (value.length < 8) { 10 | throw ArgumentError('Password must be at least 8 characters'); 11 | } 12 | 13 | final RegExp passwordRegExp = RegExp( 14 | r'^[a-zA-Z0-9]*$', 15 | ); 16 | 17 | if (!passwordRegExp.hasMatch(value)) { 18 | throw ArgumentError('Password can only contain alphanumeric characters'); 19 | } 20 | } 21 | factory Password([void Function(PasswordBuilder) updates]) = _$Password; 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/features/auth/domain/value_objects/password.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'password.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | class _$Password extends Password { 10 | @override 11 | final String value; 12 | 13 | factory _$Password([void Function(PasswordBuilder)? updates]) => 14 | (new PasswordBuilder()..update(updates))._build(); 15 | 16 | _$Password._({required this.value}) : super._() { 17 | BuiltValueNullFieldError.checkNotNull(value, r'Password', 'value'); 18 | } 19 | 20 | @override 21 | Password rebuild(void Function(PasswordBuilder) updates) => 22 | (toBuilder()..update(updates)).build(); 23 | 24 | @override 25 | PasswordBuilder toBuilder() => new PasswordBuilder()..replace(this); 26 | 27 | @override 28 | bool operator ==(Object other) { 29 | if (identical(other, this)) return true; 30 | return other is Password && value == other.value; 31 | } 32 | 33 | @override 34 | int get hashCode { 35 | var _$hash = 0; 36 | _$hash = $jc(_$hash, value.hashCode); 37 | _$hash = $jf(_$hash); 38 | return _$hash; 39 | } 40 | 41 | @override 42 | String toString() { 43 | return (newBuiltValueToStringHelper(r'Password')..add('value', value)) 44 | .toString(); 45 | } 46 | } 47 | 48 | class PasswordBuilder implements Builder { 49 | _$Password? _$v; 50 | 51 | String? _value; 52 | String? get value => _$this._value; 53 | set value(String? value) => _$this._value = value; 54 | 55 | PasswordBuilder(); 56 | 57 | PasswordBuilder get _$this { 58 | final $v = _$v; 59 | if ($v != null) { 60 | _value = $v.value; 61 | _$v = null; 62 | } 63 | return this; 64 | } 65 | 66 | @override 67 | void replace(Password other) { 68 | ArgumentError.checkNotNull(other, 'other'); 69 | _$v = other as _$Password; 70 | } 71 | 72 | @override 73 | void update(void Function(PasswordBuilder)? updates) { 74 | if (updates != null) updates(this); 75 | } 76 | 77 | @override 78 | Password build() => _build(); 79 | 80 | _$Password _build() { 81 | final _$result = _$v ?? 82 | new _$Password._( 83 | value: BuiltValueNullFieldError.checkNotNull( 84 | value, r'Password', 'value')); 85 | replace(_$result); 86 | return _$result; 87 | } 88 | } 89 | 90 | // ignore_for_file: deprecated_member_use_from_same_package,type=lint 91 | -------------------------------------------------------------------------------- /lib/src/features/auth/presentation/blocs/email_status.dart: -------------------------------------------------------------------------------- 1 | enum EmailStatus { unknown, valid, invalid } 2 | -------------------------------------------------------------------------------- /lib/src/features/auth/presentation/blocs/form_status.dart: -------------------------------------------------------------------------------- 1 | enum FormStatus { 2 | initial, 3 | valid, 4 | invalid, 5 | submissionInProgress, 6 | submissionSuccess, 7 | submissionFailure, 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/features/auth/presentation/blocs/password_status.dart: -------------------------------------------------------------------------------- 1 | enum PasswordStatus { unknown, valid, invalid } 2 | -------------------------------------------------------------------------------- /lib/src/features/auth/presentation/blocs/sign_in/sign_in_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | import '../../../domain/use_cases/sign_in_use_case.dart'; 5 | import '../../../domain/value_objects/email.dart'; 6 | import '../../../domain/value_objects/password.dart'; 7 | import '../email_status.dart'; 8 | import '../form_status.dart'; 9 | import '../password_status.dart'; 10 | 11 | part 'sign_in_state.dart'; 12 | 13 | class SignInCubit extends Cubit { 14 | final SignInUseCase _signInUseCase; 15 | 16 | SignInCubit({ 17 | required SignInUseCase signInUseCase, 18 | }) : _signInUseCase = signInUseCase, 19 | super(const SignInState()); 20 | 21 | void emailChanged(String value) { 22 | try { 23 | Email email = Email((email) => email..value = value); 24 | emit( 25 | state.copyWith( 26 | email: email, 27 | emailStatus: EmailStatus.valid, 28 | ), 29 | ); 30 | } on ArgumentError { 31 | emit(state.copyWith(emailStatus: EmailStatus.invalid)); 32 | } 33 | } 34 | 35 | void passwordChanged(String value) { 36 | try { 37 | Password password = Password((password) => password..value = value); 38 | emit( 39 | state.copyWith( 40 | password: password, 41 | passwordStatus: PasswordStatus.valid, 42 | ), 43 | ); 44 | } on ArgumentError { 45 | emit(state.copyWith(passwordStatus: PasswordStatus.invalid)); 46 | } 47 | } 48 | 49 | Future signIn() async { 50 | if (!(state.emailStatus == EmailStatus.valid) || 51 | !(state.passwordStatus == PasswordStatus.valid)) { 52 | emit(state.copyWith(formStatus: FormStatus.invalid)); 53 | emit(state.copyWith(formStatus: FormStatus.initial)); 54 | return; 55 | } 56 | 57 | emit(state.copyWith(formStatus: FormStatus.submissionInProgress)); 58 | try { 59 | await _signInUseCase( 60 | SignInParams( 61 | email: state.email!, 62 | password: state.password!, 63 | ), 64 | ); 65 | emit(state.copyWith(formStatus: FormStatus.submissionSuccess)); 66 | } catch (err) { 67 | emit(state.copyWith(formStatus: FormStatus.submissionFailure)); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/features/auth/presentation/blocs/sign_in/sign_in_state.dart: -------------------------------------------------------------------------------- 1 | part of 'sign_in_cubit.dart'; 2 | 3 | class SignInState extends Equatable { 4 | final Email? email; 5 | final Password? password; 6 | final EmailStatus emailStatus; 7 | final PasswordStatus passwordStatus; 8 | final FormStatus formStatus; 9 | 10 | const SignInState({ 11 | this.email, 12 | this.password, 13 | this.emailStatus = EmailStatus.unknown, 14 | this.passwordStatus = PasswordStatus.unknown, 15 | this.formStatus = FormStatus.initial, 16 | }); 17 | 18 | SignInState copyWith({ 19 | Email? email, 20 | Password? password, 21 | EmailStatus? emailStatus, 22 | PasswordStatus? passwordStatus, 23 | FormStatus? formStatus, 24 | }) { 25 | return SignInState( 26 | email: email ?? this.email, 27 | password: password ?? this.password, 28 | emailStatus: emailStatus ?? this.emailStatus, 29 | passwordStatus: passwordStatus ?? this.passwordStatus, 30 | formStatus: formStatus ?? this.formStatus, 31 | ); 32 | } 33 | 34 | @override 35 | List get props => [ 36 | email, 37 | password, 38 | emailStatus, 39 | passwordStatus, 40 | formStatus, 41 | ]; 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/features/auth/presentation/blocs/sign_up/sign_up_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | import '../../../domain/use_cases/sign_up_use_case.dart'; 5 | import '../../../domain/value_objects/email.dart'; 6 | import '../../../domain/value_objects/password.dart'; 7 | import '../email_status.dart'; 8 | import '../form_status.dart'; 9 | import '../password_status.dart'; 10 | 11 | part 'sign_up_state.dart'; 12 | 13 | class SignUpCubit extends Cubit { 14 | final SignUpUseCase _signUpUseCase; 15 | 16 | SignUpCubit({ 17 | required SignUpUseCase signUpUseCase, 18 | }) : _signUpUseCase = signUpUseCase, 19 | super(const SignUpState()); 20 | 21 | void emailChanged(String value) { 22 | try { 23 | Email email = Email((email) => email..value = value); 24 | emit( 25 | state.copyWith( 26 | email: email, 27 | emailStatus: EmailStatus.valid, 28 | ), 29 | ); 30 | } on ArgumentError { 31 | emit(state.copyWith(emailStatus: EmailStatus.invalid)); 32 | } 33 | } 34 | 35 | void passwordChanged(String value) { 36 | try { 37 | Password password = Password((password) => password..value = value); 38 | emit( 39 | state.copyWith( 40 | password: password, 41 | passwordStatus: PasswordStatus.valid, 42 | ), 43 | ); 44 | } on ArgumentError { 45 | emit(state.copyWith(passwordStatus: PasswordStatus.invalid)); 46 | } 47 | } 48 | 49 | Future signUp() async { 50 | if (!(state.emailStatus == EmailStatus.valid) || 51 | !(state.passwordStatus == PasswordStatus.valid)) { 52 | emit(state.copyWith(formStatus: FormStatus.invalid)); 53 | emit(state.copyWith(formStatus: FormStatus.initial)); 54 | return; 55 | } 56 | 57 | emit(state.copyWith(formStatus: FormStatus.submissionInProgress)); 58 | try { 59 | await _signUpUseCase( 60 | SignUpParams(email: state.email!, password: state.password!), 61 | ); 62 | emit(state.copyWith(formStatus: FormStatus.submissionSuccess)); 63 | } catch (err) { 64 | emit(state.copyWith(formStatus: FormStatus.submissionFailure)); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/features/auth/presentation/blocs/sign_up/sign_up_state.dart: -------------------------------------------------------------------------------- 1 | part of 'sign_up_cubit.dart'; 2 | 3 | class SignUpState extends Equatable { 4 | final Email? email; 5 | final Password? password; 6 | final EmailStatus emailStatus; 7 | final PasswordStatus passwordStatus; 8 | final FormStatus formStatus; 9 | 10 | const SignUpState({ 11 | this.email, 12 | this.password, 13 | this.emailStatus = EmailStatus.unknown, 14 | this.passwordStatus = PasswordStatus.unknown, 15 | this.formStatus = FormStatus.initial, 16 | }); 17 | 18 | SignUpState copyWith({ 19 | Email? email, 20 | Password? password, 21 | EmailStatus? emailStatus, 22 | PasswordStatus? passwordStatus, 23 | FormStatus? formStatus, 24 | }) { 25 | return SignUpState( 26 | email: email ?? this.email, 27 | password: password ?? this.password, 28 | emailStatus: emailStatus ?? this.emailStatus, 29 | passwordStatus: passwordStatus ?? this.passwordStatus, 30 | formStatus: formStatus ?? this.formStatus, 31 | ); 32 | } 33 | 34 | @override 35 | List get props => [ 36 | email, 37 | password, 38 | emailStatus, 39 | passwordStatus, 40 | formStatus, 41 | ]; 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/features/auth/presentation/screens/sign_in_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | 6 | import '../../domain/repositories/auth_repository.dart'; 7 | import '../../domain/use_cases/sign_in_use_case.dart'; 8 | import '../blocs/email_status.dart'; 9 | import '../blocs/form_status.dart'; 10 | import '../blocs/password_status.dart'; 11 | import '../blocs/sign_in/sign_in_cubit.dart'; 12 | 13 | class SignInScreen extends StatelessWidget { 14 | const SignInScreen({super.key}); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return BlocProvider( 19 | create: (context) => SignInCubit( 20 | signInUseCase: SignInUseCase( 21 | authRepository: context.read(), 22 | ), 23 | ), 24 | child: const SignInView(), 25 | ); 26 | } 27 | } 28 | 29 | class SignInView extends StatefulWidget { 30 | const SignInView({super.key}); 31 | 32 | @override 33 | State createState() => _SignInViewState(); 34 | } 35 | 36 | class _SignInViewState extends State { 37 | Timer? debounce; 38 | 39 | @override 40 | void dispose() { 41 | debounce?.cancel(); 42 | super.dispose(); 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | return Scaffold( 48 | appBar: AppBar( 49 | title: const Text('Sign In'), 50 | ), 51 | body: BlocConsumer( 52 | listener: (context, state) { 53 | if (state.formStatus == FormStatus.invalid) { 54 | ScaffoldMessenger.of(context) 55 | ..hideCurrentSnackBar() 56 | ..showSnackBar( 57 | const SnackBar( 58 | content: Text('Invalid form: please fill in all fields'), 59 | ), 60 | ); 61 | } 62 | if (state.formStatus == FormStatus.submissionFailure) { 63 | ScaffoldMessenger.of(context) 64 | ..hideCurrentSnackBar() 65 | ..showSnackBar( 66 | const SnackBar( 67 | content: Text( 68 | 'There was an error with the sign in process. Try again.', 69 | ), 70 | ), 71 | ); 72 | } 73 | }, 74 | builder: (context, state) { 75 | return Padding( 76 | padding: const EdgeInsets.all(16.0), 77 | child: Column( 78 | children: [ 79 | TextFormField( 80 | key: const Key('signIn_emailInput_textField'), 81 | decoration: InputDecoration( 82 | labelText: 'Email', 83 | errorText: state.emailStatus == EmailStatus.invalid 84 | ? 'Invalid email' 85 | : null, 86 | ), 87 | onChanged: (String value) { 88 | if (debounce?.isActive ?? false) debounce?.cancel(); 89 | debounce = Timer(const Duration(milliseconds: 500), () { 90 | context.read().emailChanged(value); 91 | }); 92 | }, 93 | ), 94 | TextFormField( 95 | key: const Key('signIn_passwordInput_textField'), 96 | obscureText: true, 97 | decoration: InputDecoration( 98 | labelText: 'Password', 99 | errorText: state.passwordStatus == PasswordStatus.invalid 100 | ? 'Invalid password' 101 | : null, 102 | ), 103 | onChanged: (String value) { 104 | context.read().passwordChanged(value); 105 | }, 106 | ), 107 | const SizedBox(height: 8.0), 108 | ElevatedButton( 109 | key: const Key('signIn_continue_elevatedButton'), 110 | onPressed: context.read().state.formStatus == 111 | FormStatus.submissionInProgress 112 | ? null 113 | : () { 114 | context.read().signIn(); 115 | }, 116 | child: const Text('Sign In'), 117 | ), 118 | ], 119 | ), 120 | ); 121 | }, 122 | ), 123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lib/src/features/auth/presentation/screens/sign_up_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | 6 | import '../../domain/repositories/auth_repository.dart'; 7 | import '../../domain/use_cases/sign_up_use_case.dart'; 8 | import '../blocs/email_status.dart'; 9 | import '../blocs/form_status.dart'; 10 | import '../blocs/password_status.dart'; 11 | import '../blocs/sign_up/sign_up_cubit.dart'; 12 | 13 | class SignUpScreen extends StatelessWidget { 14 | const SignUpScreen({super.key}); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return BlocProvider( 19 | create: (context) => SignUpCubit( 20 | signUpUseCase: SignUpUseCase( 21 | authRepository: context.read(), 22 | ), 23 | ), 24 | child: const SignUpView(), 25 | ); 26 | } 27 | } 28 | 29 | class SignUpView extends StatefulWidget { 30 | const SignUpView({super.key}); 31 | 32 | @override 33 | State createState() => _SignUpViewState(); 34 | } 35 | 36 | class _SignUpViewState extends State { 37 | Timer? debounce; 38 | 39 | @override 40 | void dispose() { 41 | debounce?.cancel(); 42 | super.dispose(); 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | return Scaffold( 48 | appBar: AppBar( 49 | title: const Text('Sign Up'), 50 | ), 51 | body: BlocConsumer( 52 | listener: (context, state) { 53 | if (state.formStatus == FormStatus.invalid) { 54 | ScaffoldMessenger.of(context) 55 | ..hideCurrentSnackBar() 56 | ..showSnackBar( 57 | const SnackBar( 58 | content: Text('Invalid form: please fill in all fields'), 59 | ), 60 | ); 61 | } 62 | if (state.formStatus == FormStatus.submissionFailure) { 63 | ScaffoldMessenger.of(context) 64 | ..hideCurrentSnackBar() 65 | ..showSnackBar( 66 | const SnackBar( 67 | content: Text( 68 | 'There was an error with the sign up process. Try again.', 69 | ), 70 | ), 71 | ); 72 | } 73 | }, 74 | builder: (context, state) { 75 | return Padding( 76 | padding: const EdgeInsets.all(16.0), 77 | child: Column( 78 | children: [ 79 | TextFormField( 80 | key: const Key('signUp_emailInput_textField'), 81 | decoration: InputDecoration( 82 | labelText: 'Email', 83 | errorText: state.emailStatus == EmailStatus.invalid 84 | ? 'Invalid email' 85 | : null, 86 | ), 87 | onChanged: (String value) { 88 | if (debounce?.isActive ?? false) debounce?.cancel(); 89 | debounce = Timer(const Duration(milliseconds: 500), () { 90 | context.read().emailChanged(value); 91 | }); 92 | }, 93 | ), 94 | TextFormField( 95 | key: const Key('signUp_passwordInput_textField'), 96 | obscureText: true, 97 | decoration: InputDecoration( 98 | labelText: 'Password', 99 | errorText: state.passwordStatus == PasswordStatus.invalid 100 | ? 'Invalid password' 101 | : null, 102 | ), 103 | onChanged: (String value) { 104 | context.read().passwordChanged(value); 105 | }, 106 | ), 107 | const SizedBox(height: 8.0), 108 | ElevatedButton( 109 | key: const Key('signUp_continue_elevatedButton'), 110 | onPressed: context.read().state.formStatus == 111 | FormStatus.submissionInProgress 112 | ? null 113 | : () { 114 | context.read().signUp(); 115 | }, 116 | child: const Text('Sign Up'), 117 | ), 118 | ], 119 | ), 120 | ); 121 | }, 122 | ), 123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_clean_architecture_with_firebase 2 | description: A new Flutter project. 3 | publish_to: 'none' 4 | 5 | version: 1.0.0+1 6 | 7 | environment: 8 | sdk: '>=3.0.0 <4.0.0' 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | cupertino_icons: ^1.0.2 14 | firebase_core: ^2.12.0 15 | firebase_auth: ^4.6.0 16 | cloud_firestore: ^4.7.1 17 | equatable: ^2.0.5 18 | flutter_bloc: ^8.1.2 19 | bloc_test: ^9.1.1 20 | go_router: ^7.0.1 21 | hive: ^2.2.3 22 | hive_flutter: ^1.1.0 23 | built_value: ^8.5.0 24 | flex_seed_scheme: ^1.3.0 25 | google_fonts: ^4.0.4 26 | connectivity_plus: ^4.0.0 27 | network_image_mock: ^2.1.1 28 | uuid: ^3.0.7 29 | intl: ^0.18.1 30 | fpdart: ^0.6.0 31 | 32 | dev_dependencies: 33 | flutter_test: 34 | sdk: flutter 35 | flutter_lints: ^2.0.0 36 | mockito: ^5.4.0 37 | build_runner: ^2.4.4 38 | built_value_generator: ^8.5.0 39 | hive_generator: ^2.0.0 40 | 41 | flutter: 42 | uses-material-design: true 43 | -------------------------------------------------------------------------------- /screenshots/clean_architecture_course.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/screenshots/clean_architecture_course.png -------------------------------------------------------------------------------- /test/src/features/auth/data/data_sources/auth_local_data_source_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/data/data_sources/auth_local_data_source.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | late AuthLocalDataSource authLocalDataSource; 6 | 7 | setUp(() { 8 | authLocalDataSource = AuthLocalDataSource(); 9 | }); 10 | 11 | group('AuthLocalDataSource', () { 12 | test('should write a value to the data source', () { 13 | const key = 'test_key'; 14 | const value = 'test_value'; 15 | authLocalDataSource.write(key: key, value: value); 16 | 17 | expect(authLocalDataSource.read(key: key), value); 18 | }); 19 | 20 | test('should overwrite a value in the data source', () { 21 | const key = 'test_key'; 22 | const initialValue = 'initial_value'; 23 | authLocalDataSource.write(key: key, value: initialValue); 24 | 25 | const newValue = 'new_value'; 26 | authLocalDataSource.write(key: key, value: newValue); 27 | 28 | expect(authLocalDataSource.read(key: key), newValue); 29 | }); 30 | 31 | test('should read a value from the data source', () { 32 | const key = 'test_key'; 33 | const value = 'test_value'; 34 | authLocalDataSource.write(key: key, value: value); 35 | 36 | final result = authLocalDataSource.read(key: key); 37 | 38 | expect(result, value); 39 | }); 40 | 41 | test('should return null if no value is found for the key', () { 42 | const key = 'non_existent_key'; 43 | 44 | final result = authLocalDataSource.read(key: key); 45 | 46 | expect(result, isNull); 47 | }); 48 | 49 | test('should return null if value is not of type T', () { 50 | const key = 'test_key'; 51 | const value = 123; 52 | authLocalDataSource.write(key: key, value: value); 53 | 54 | final result = authLocalDataSource.read(key: key); 55 | 56 | expect(result, isNull); 57 | }); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /test/src/features/auth/data/data_sources/auth_remote_data_source_firebase_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; 2 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/data/data_sources/auth_remote_data_source_firebase.dart'; 3 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/data/models/auth_user_model.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:mockito/annotations.dart'; 6 | import 'package:mockito/mockito.dart'; 7 | 8 | import 'auth_remote_data_source_firebase_test.mocks.dart'; 9 | 10 | @GenerateMocks([ 11 | firebase_auth.FirebaseAuth, 12 | firebase_auth.UserCredential, 13 | firebase_auth.User, 14 | ]) 15 | void main() { 16 | late MockFirebaseAuth mockFirebaseAuth; 17 | late MockUserCredential mockUserCredential; 18 | late MockUser mockUser; 19 | late AuthRemoteDataSourceFirebase authRemoteDataSource; 20 | late AuthUserModel authUserModel; 21 | 22 | const tEmail = 'test@test.com'; 23 | const tPassword = 'password'; 24 | 25 | setUp(() { 26 | mockFirebaseAuth = MockFirebaseAuth(); 27 | mockUserCredential = MockUserCredential(); 28 | mockUser = MockUser(); 29 | when(mockUser.uid).thenReturn('test_uid'); 30 | when(mockUser.email).thenReturn('test_email'); 31 | when(mockUser.displayName).thenReturn('test_username'); 32 | when(mockUser.photoURL).thenReturn('test_photoURL'); 33 | authUserModel = AuthUserModel.fromFirebaseAuthUser(mockUser); 34 | authRemoteDataSource = AuthRemoteDataSourceFirebase( 35 | firebaseAuth: mockFirebaseAuth, 36 | ); 37 | }); 38 | 39 | group('signUpWithEmailAndPassword', () { 40 | test( 41 | 'should return AuthUserModel when signUpWithEmailAndPassword is successful', 42 | () async { 43 | when( 44 | mockFirebaseAuth.createUserWithEmailAndPassword( 45 | email: anyNamed('email'), 46 | password: anyNamed('password'), 47 | ), 48 | ).thenAnswer((_) async => mockUserCredential); 49 | when(mockUserCredential.user).thenReturn(mockUser); 50 | 51 | final result = await authRemoteDataSource.signUpWithEmailAndPassword( 52 | email: tEmail, 53 | password: tPassword, 54 | ); 55 | 56 | expect(result, equals(authUserModel)); 57 | }); 58 | 59 | test('should throw Exception when signUpWithEmailAndPassword fails', 60 | () async { 61 | when(mockFirebaseAuth.createUserWithEmailAndPassword( 62 | email: anyNamed('email'), 63 | password: anyNamed('password'), 64 | )).thenThrow(Exception()); 65 | 66 | final call = authRemoteDataSource.signUpWithEmailAndPassword; 67 | 68 | expect( 69 | () => call(email: tEmail, password: tPassword), 70 | throwsA(isA()), 71 | ); 72 | }); 73 | }); 74 | 75 | group('signInWithEmailAndPassword', () { 76 | test( 77 | 'should call signInWithEmailAndPassword on FirebaseAuth with correct email and password', 78 | () async { 79 | when(mockFirebaseAuth.signInWithEmailAndPassword( 80 | email: anyNamed('email'), 81 | password: anyNamed('password'), 82 | )).thenAnswer((_) async => mockUserCredential); 83 | when(mockUserCredential.user).thenReturn(mockUser); 84 | 85 | final result = await authRemoteDataSource.signInWithEmailAndPassword( 86 | email: 'test@test.com', 87 | password: 'test_password', 88 | ); 89 | 90 | expect(result, equals(authUserModel)); 91 | 92 | verify(mockFirebaseAuth.signInWithEmailAndPassword( 93 | email: 'test@test.com', 94 | password: 'test_password', 95 | )); 96 | }, 97 | ); 98 | 99 | test( 100 | 'should throw an Exception when FirebaseAuth throws an exception', 101 | () async { 102 | when(mockFirebaseAuth.signInWithEmailAndPassword( 103 | email: anyNamed('email'), 104 | password: anyNamed('password'), 105 | )).thenThrow(Exception('Sign in failed: test error')); 106 | 107 | final call = authRemoteDataSource.signInWithEmailAndPassword; 108 | 109 | expect( 110 | () => call( 111 | email: 'test@test.com', 112 | password: 'test_password', 113 | ), 114 | throwsA(isA()), 115 | ); 116 | }, 117 | ); 118 | }); 119 | 120 | group('signOut', () { 121 | test( 122 | 'should call signOut on FirebaseAuth', 123 | () async { 124 | when(mockFirebaseAuth.signOut()).thenAnswer((_) async {}); 125 | 126 | await authRemoteDataSource.signOut(); 127 | 128 | verify(mockFirebaseAuth.signOut()); 129 | }, 130 | ); 131 | 132 | test( 133 | 'should throw an Exception when FirebaseAuth throws an exception', 134 | () async { 135 | when(mockFirebaseAuth.signOut()) 136 | .thenThrow(Exception('Sign out failed: test error')); 137 | 138 | final call = authRemoteDataSource.signOut; 139 | 140 | expect( 141 | () => call(), 142 | throwsA(isA()), 143 | ); 144 | }, 145 | ); 146 | }); 147 | } 148 | -------------------------------------------------------------------------------- /test/src/features/auth/data/models/auth_user_model_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/data/models/auth_user_model.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:mockito/annotations.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | 7 | import 'auth_user_model_test.mocks.dart'; 8 | 9 | @GenerateMocks([firebase_auth.User]) 10 | void main() { 11 | late MockUser mockUser; 12 | 13 | setUp(() { 14 | mockUser = MockUser(); 15 | 16 | when(mockUser.uid).thenReturn('testId'); 17 | when(mockUser.email).thenReturn('test@test.com'); 18 | when(mockUser.displayName).thenReturn('Test User'); 19 | when(mockUser.photoURL).thenReturn('http://example.com/photo.jpg'); 20 | }); 21 | 22 | const id = 'testId'; 23 | const email = 'test@test.com'; 24 | const name = 'Test User'; 25 | const photoURL = 'http://example.com/photo.jpg'; 26 | 27 | const authUserModel = AuthUserModel( 28 | id: id, 29 | email: email, 30 | name: name, 31 | photoURL: photoURL, 32 | ); 33 | 34 | group('AuthUserModel', () { 35 | test('properties are correctly assigned on creation', () { 36 | expect(authUserModel.id, equals(id)); 37 | expect(authUserModel.email, equals(email)); 38 | expect(authUserModel.name, equals(name)); 39 | expect(authUserModel.photoURL, equals(photoURL)); 40 | }); 41 | 42 | test('creates AuthUserModel from FirebaseUser', () { 43 | final authUserModel = AuthUserModel.fromFirebaseAuthUser(mockUser); 44 | 45 | expect(authUserModel.id, equals(mockUser.uid)); 46 | expect(authUserModel.email, equals(mockUser.email)); 47 | expect(authUserModel.name, equals(mockUser.displayName)); 48 | expect(authUserModel.photoURL, equals(mockUser.photoURL)); 49 | }); 50 | 51 | test('converts to entity correctly', () { 52 | final authUser = authUserModel.toEntity(); 53 | 54 | expect(authUser.id, equals(id)); 55 | expect(authUser.email, equals(email)); 56 | expect(authUser.name, equals(name)); 57 | expect(authUser.photoURL, equals(photoURL)); 58 | }); 59 | 60 | test('get props returns a list with all properties', () { 61 | final props = authUserModel.props; 62 | 63 | expect(props, containsAll([id, email, name, photoURL])); 64 | }); 65 | 66 | test('handles null values in firebase user correctly', () { 67 | when(mockUser.email).thenReturn(''); 68 | when(mockUser.displayName).thenReturn(null); 69 | when(mockUser.photoURL).thenReturn(null); 70 | 71 | final authUserModel = AuthUserModel.fromFirebaseAuthUser(mockUser); 72 | 73 | expect(authUserModel.email, equals('')); 74 | expect(authUserModel.name, isNull); 75 | expect(authUserModel.photoURL, isNull); 76 | }); 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /test/src/features/auth/data/models/auth_user_model_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.4.0 from annotations 2 | // in flutter_clean_architecture_with_firebase/test/src/features/auth/data/models/auth_user_model_test.dart. 3 | // Do not manually edit this file. 4 | 5 | // ignore_for_file: no_leading_underscores_for_library_prefixes 6 | import 'dart:async' as _i4; 7 | 8 | import 'package:firebase_auth/firebase_auth.dart' as _i3; 9 | import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart' 10 | as _i2; 11 | import 'package:mockito/mockito.dart' as _i1; 12 | 13 | // ignore_for_file: type=lint 14 | // ignore_for_file: avoid_redundant_argument_values 15 | // ignore_for_file: avoid_setters_without_getters 16 | // ignore_for_file: comment_references 17 | // ignore_for_file: implementation_imports 18 | // ignore_for_file: invalid_use_of_visible_for_testing_member 19 | // ignore_for_file: prefer_const_constructors 20 | // ignore_for_file: unnecessary_parenthesis 21 | // ignore_for_file: camel_case_types 22 | // ignore_for_file: subtype_of_sealed_class 23 | 24 | class _FakeUserMetadata_0 extends _i1.SmartFake implements _i2.UserMetadata { 25 | _FakeUserMetadata_0( 26 | Object parent, 27 | Invocation parentInvocation, 28 | ) : super( 29 | parent, 30 | parentInvocation, 31 | ); 32 | } 33 | 34 | class _FakeMultiFactor_1 extends _i1.SmartFake implements _i3.MultiFactor { 35 | _FakeMultiFactor_1( 36 | Object parent, 37 | Invocation parentInvocation, 38 | ) : super( 39 | parent, 40 | parentInvocation, 41 | ); 42 | } 43 | 44 | class _FakeIdTokenResult_2 extends _i1.SmartFake implements _i2.IdTokenResult { 45 | _FakeIdTokenResult_2( 46 | Object parent, 47 | Invocation parentInvocation, 48 | ) : super( 49 | parent, 50 | parentInvocation, 51 | ); 52 | } 53 | 54 | class _FakeUserCredential_3 extends _i1.SmartFake 55 | implements _i3.UserCredential { 56 | _FakeUserCredential_3( 57 | Object parent, 58 | Invocation parentInvocation, 59 | ) : super( 60 | parent, 61 | parentInvocation, 62 | ); 63 | } 64 | 65 | class _FakeConfirmationResult_4 extends _i1.SmartFake 66 | implements _i3.ConfirmationResult { 67 | _FakeConfirmationResult_4( 68 | Object parent, 69 | Invocation parentInvocation, 70 | ) : super( 71 | parent, 72 | parentInvocation, 73 | ); 74 | } 75 | 76 | class _FakeUser_5 extends _i1.SmartFake implements _i3.User { 77 | _FakeUser_5( 78 | Object parent, 79 | Invocation parentInvocation, 80 | ) : super( 81 | parent, 82 | parentInvocation, 83 | ); 84 | } 85 | 86 | /// A class which mocks [User]. 87 | /// 88 | /// See the documentation for Mockito's code generation for more information. 89 | class MockUser extends _i1.Mock implements _i3.User { 90 | MockUser() { 91 | _i1.throwOnMissingStub(this); 92 | } 93 | 94 | @override 95 | bool get emailVerified => (super.noSuchMethod( 96 | Invocation.getter(#emailVerified), 97 | returnValue: false, 98 | ) as bool); 99 | @override 100 | bool get isAnonymous => (super.noSuchMethod( 101 | Invocation.getter(#isAnonymous), 102 | returnValue: false, 103 | ) as bool); 104 | @override 105 | _i2.UserMetadata get metadata => (super.noSuchMethod( 106 | Invocation.getter(#metadata), 107 | returnValue: _FakeUserMetadata_0( 108 | this, 109 | Invocation.getter(#metadata), 110 | ), 111 | ) as _i2.UserMetadata); 112 | @override 113 | List<_i2.UserInfo> get providerData => (super.noSuchMethod( 114 | Invocation.getter(#providerData), 115 | returnValue: <_i2.UserInfo>[], 116 | ) as List<_i2.UserInfo>); 117 | @override 118 | String get uid => (super.noSuchMethod( 119 | Invocation.getter(#uid), 120 | returnValue: '', 121 | ) as String); 122 | @override 123 | _i3.MultiFactor get multiFactor => (super.noSuchMethod( 124 | Invocation.getter(#multiFactor), 125 | returnValue: _FakeMultiFactor_1( 126 | this, 127 | Invocation.getter(#multiFactor), 128 | ), 129 | ) as _i3.MultiFactor); 130 | @override 131 | _i4.Future delete() => (super.noSuchMethod( 132 | Invocation.method( 133 | #delete, 134 | [], 135 | ), 136 | returnValue: _i4.Future.value(), 137 | returnValueForMissingStub: _i4.Future.value(), 138 | ) as _i4.Future); 139 | @override 140 | _i4.Future getIdToken([bool? forceRefresh = false]) => 141 | (super.noSuchMethod( 142 | Invocation.method( 143 | #getIdToken, 144 | [forceRefresh], 145 | ), 146 | returnValue: _i4.Future.value(''), 147 | ) as _i4.Future); 148 | @override 149 | _i4.Future<_i2.IdTokenResult> getIdTokenResult( 150 | [bool? forceRefresh = false]) => 151 | (super.noSuchMethod( 152 | Invocation.method( 153 | #getIdTokenResult, 154 | [forceRefresh], 155 | ), 156 | returnValue: _i4.Future<_i2.IdTokenResult>.value(_FakeIdTokenResult_2( 157 | this, 158 | Invocation.method( 159 | #getIdTokenResult, 160 | [forceRefresh], 161 | ), 162 | )), 163 | ) as _i4.Future<_i2.IdTokenResult>); 164 | @override 165 | _i4.Future<_i3.UserCredential> linkWithCredential( 166 | _i2.AuthCredential? credential) => 167 | (super.noSuchMethod( 168 | Invocation.method( 169 | #linkWithCredential, 170 | [credential], 171 | ), 172 | returnValue: _i4.Future<_i3.UserCredential>.value(_FakeUserCredential_3( 173 | this, 174 | Invocation.method( 175 | #linkWithCredential, 176 | [credential], 177 | ), 178 | )), 179 | ) as _i4.Future<_i3.UserCredential>); 180 | @override 181 | _i4.Future<_i3.UserCredential> linkWithProvider(_i2.AuthProvider? provider) => 182 | (super.noSuchMethod( 183 | Invocation.method( 184 | #linkWithProvider, 185 | [provider], 186 | ), 187 | returnValue: _i4.Future<_i3.UserCredential>.value(_FakeUserCredential_3( 188 | this, 189 | Invocation.method( 190 | #linkWithProvider, 191 | [provider], 192 | ), 193 | )), 194 | ) as _i4.Future<_i3.UserCredential>); 195 | @override 196 | _i4.Future<_i3.UserCredential> reauthenticateWithProvider( 197 | _i2.AuthProvider? provider) => 198 | (super.noSuchMethod( 199 | Invocation.method( 200 | #reauthenticateWithProvider, 201 | [provider], 202 | ), 203 | returnValue: _i4.Future<_i3.UserCredential>.value(_FakeUserCredential_3( 204 | this, 205 | Invocation.method( 206 | #reauthenticateWithProvider, 207 | [provider], 208 | ), 209 | )), 210 | ) as _i4.Future<_i3.UserCredential>); 211 | @override 212 | _i4.Future<_i3.UserCredential> reauthenticateWithPopup( 213 | _i2.AuthProvider? provider) => 214 | (super.noSuchMethod( 215 | Invocation.method( 216 | #reauthenticateWithPopup, 217 | [provider], 218 | ), 219 | returnValue: _i4.Future<_i3.UserCredential>.value(_FakeUserCredential_3( 220 | this, 221 | Invocation.method( 222 | #reauthenticateWithPopup, 223 | [provider], 224 | ), 225 | )), 226 | ) as _i4.Future<_i3.UserCredential>); 227 | @override 228 | _i4.Future reauthenticateWithRedirect(_i2.AuthProvider? provider) => 229 | (super.noSuchMethod( 230 | Invocation.method( 231 | #reauthenticateWithRedirect, 232 | [provider], 233 | ), 234 | returnValue: _i4.Future.value(), 235 | returnValueForMissingStub: _i4.Future.value(), 236 | ) as _i4.Future); 237 | @override 238 | _i4.Future<_i3.UserCredential> linkWithPopup(_i2.AuthProvider? provider) => 239 | (super.noSuchMethod( 240 | Invocation.method( 241 | #linkWithPopup, 242 | [provider], 243 | ), 244 | returnValue: _i4.Future<_i3.UserCredential>.value(_FakeUserCredential_3( 245 | this, 246 | Invocation.method( 247 | #linkWithPopup, 248 | [provider], 249 | ), 250 | )), 251 | ) as _i4.Future<_i3.UserCredential>); 252 | @override 253 | _i4.Future linkWithRedirect(_i2.AuthProvider? provider) => 254 | (super.noSuchMethod( 255 | Invocation.method( 256 | #linkWithRedirect, 257 | [provider], 258 | ), 259 | returnValue: _i4.Future.value(), 260 | returnValueForMissingStub: _i4.Future.value(), 261 | ) as _i4.Future); 262 | @override 263 | _i4.Future<_i3.ConfirmationResult> linkWithPhoneNumber( 264 | String? phoneNumber, [ 265 | _i3.RecaptchaVerifier? verifier, 266 | ]) => 267 | (super.noSuchMethod( 268 | Invocation.method( 269 | #linkWithPhoneNumber, 270 | [ 271 | phoneNumber, 272 | verifier, 273 | ], 274 | ), 275 | returnValue: 276 | _i4.Future<_i3.ConfirmationResult>.value(_FakeConfirmationResult_4( 277 | this, 278 | Invocation.method( 279 | #linkWithPhoneNumber, 280 | [ 281 | phoneNumber, 282 | verifier, 283 | ], 284 | ), 285 | )), 286 | ) as _i4.Future<_i3.ConfirmationResult>); 287 | @override 288 | _i4.Future<_i3.UserCredential> reauthenticateWithCredential( 289 | _i2.AuthCredential? credential) => 290 | (super.noSuchMethod( 291 | Invocation.method( 292 | #reauthenticateWithCredential, 293 | [credential], 294 | ), 295 | returnValue: _i4.Future<_i3.UserCredential>.value(_FakeUserCredential_3( 296 | this, 297 | Invocation.method( 298 | #reauthenticateWithCredential, 299 | [credential], 300 | ), 301 | )), 302 | ) as _i4.Future<_i3.UserCredential>); 303 | @override 304 | _i4.Future reload() => (super.noSuchMethod( 305 | Invocation.method( 306 | #reload, 307 | [], 308 | ), 309 | returnValue: _i4.Future.value(), 310 | returnValueForMissingStub: _i4.Future.value(), 311 | ) as _i4.Future); 312 | @override 313 | _i4.Future sendEmailVerification( 314 | [_i2.ActionCodeSettings? actionCodeSettings]) => 315 | (super.noSuchMethod( 316 | Invocation.method( 317 | #sendEmailVerification, 318 | [actionCodeSettings], 319 | ), 320 | returnValue: _i4.Future.value(), 321 | returnValueForMissingStub: _i4.Future.value(), 322 | ) as _i4.Future); 323 | @override 324 | _i4.Future<_i3.User> unlink(String? providerId) => (super.noSuchMethod( 325 | Invocation.method( 326 | #unlink, 327 | [providerId], 328 | ), 329 | returnValue: _i4.Future<_i3.User>.value(_FakeUser_5( 330 | this, 331 | Invocation.method( 332 | #unlink, 333 | [providerId], 334 | ), 335 | )), 336 | ) as _i4.Future<_i3.User>); 337 | @override 338 | _i4.Future updateEmail(String? newEmail) => (super.noSuchMethod( 339 | Invocation.method( 340 | #updateEmail, 341 | [newEmail], 342 | ), 343 | returnValue: _i4.Future.value(), 344 | returnValueForMissingStub: _i4.Future.value(), 345 | ) as _i4.Future); 346 | @override 347 | _i4.Future updatePassword(String? newPassword) => (super.noSuchMethod( 348 | Invocation.method( 349 | #updatePassword, 350 | [newPassword], 351 | ), 352 | returnValue: _i4.Future.value(), 353 | returnValueForMissingStub: _i4.Future.value(), 354 | ) as _i4.Future); 355 | @override 356 | _i4.Future updatePhoneNumber( 357 | _i2.PhoneAuthCredential? phoneCredential) => 358 | (super.noSuchMethod( 359 | Invocation.method( 360 | #updatePhoneNumber, 361 | [phoneCredential], 362 | ), 363 | returnValue: _i4.Future.value(), 364 | returnValueForMissingStub: _i4.Future.value(), 365 | ) as _i4.Future); 366 | @override 367 | _i4.Future updateDisplayName(String? displayName) => 368 | (super.noSuchMethod( 369 | Invocation.method( 370 | #updateDisplayName, 371 | [displayName], 372 | ), 373 | returnValue: _i4.Future.value(), 374 | returnValueForMissingStub: _i4.Future.value(), 375 | ) as _i4.Future); 376 | @override 377 | _i4.Future updatePhotoURL(String? photoURL) => (super.noSuchMethod( 378 | Invocation.method( 379 | #updatePhotoURL, 380 | [photoURL], 381 | ), 382 | returnValue: _i4.Future.value(), 383 | returnValueForMissingStub: _i4.Future.value(), 384 | ) as _i4.Future); 385 | @override 386 | _i4.Future updateProfile({ 387 | String? displayName, 388 | String? photoURL, 389 | }) => 390 | (super.noSuchMethod( 391 | Invocation.method( 392 | #updateProfile, 393 | [], 394 | { 395 | #displayName: displayName, 396 | #photoURL: photoURL, 397 | }, 398 | ), 399 | returnValue: _i4.Future.value(), 400 | returnValueForMissingStub: _i4.Future.value(), 401 | ) as _i4.Future); 402 | @override 403 | _i4.Future verifyBeforeUpdateEmail( 404 | String? newEmail, [ 405 | _i2.ActionCodeSettings? actionCodeSettings, 406 | ]) => 407 | (super.noSuchMethod( 408 | Invocation.method( 409 | #verifyBeforeUpdateEmail, 410 | [ 411 | newEmail, 412 | actionCodeSettings, 413 | ], 414 | ), 415 | returnValue: _i4.Future.value(), 416 | returnValueForMissingStub: _i4.Future.value(), 417 | ) as _i4.Future); 418 | } 419 | -------------------------------------------------------------------------------- /test/src/features/auth/data/repositories/auth_repository_impl_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/data/data_sources/auth_local_data_source.dart'; 2 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/data/data_sources/auth_remote_data_source.dart'; 3 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/data/models/auth_user_model.dart'; 4 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/data/repositories/auth_repository_impl.dart'; 5 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/entities/auth_user.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:mockito/annotations.dart'; 8 | import 'package:mockito/mockito.dart'; 9 | 10 | import 'auth_repository_impl_test.mocks.dart'; 11 | 12 | @GenerateMocks([ 13 | AuthRemoteDataSource, 14 | AuthLocalDataSource, 15 | ]) 16 | void main() { 17 | late MockAuthRemoteDataSource mockRemoteDataSource; 18 | late MockAuthLocalDataSource mockLocalDataSource; 19 | late AuthRepositoryImpl authRepository; 20 | 21 | setUp(() { 22 | mockRemoteDataSource = MockAuthRemoteDataSource(); 23 | mockLocalDataSource = MockAuthLocalDataSource(); 24 | authRepository = AuthRepositoryImpl( 25 | remoteDataSource: mockRemoteDataSource, 26 | localDataSource: mockLocalDataSource, 27 | ); 28 | }); 29 | 30 | const email = 'test@gmail.com'; 31 | const password = 'password12345'; 32 | const authUserModel = AuthUserModel(id: '123', email: 'test@test.com'); 33 | 34 | group('authUser', () { 35 | test( 36 | 'emits AuthUser.empty when remoteDataSource.user emits null' 37 | 'and clear the local user data', () async { 38 | when(mockRemoteDataSource.user).thenAnswer((_) => Stream.value(null)); 39 | 40 | final result = await authRepository.authUser.first; 41 | 42 | expect(result, AuthUser.empty); 43 | 44 | verify(mockLocalDataSource.write(key: 'user', value: null)).called(1); 45 | }); 46 | 47 | test( 48 | 'emits an AuthUser when remoteDataSource.user emits non-null value' 49 | 'and stores the user data locally', () async { 50 | when(mockRemoteDataSource.user).thenAnswer( 51 | (_) => Stream.value(authUserModel), 52 | ); 53 | 54 | final result = await authRepository.authUser.first; 55 | 56 | expect(result, authUserModel.toEntity()); 57 | 58 | verify(mockLocalDataSource.write(key: 'user', value: authUserModel)) 59 | .called(1); 60 | }); 61 | }); 62 | 63 | group('signUp', () { 64 | test( 65 | 'calls [signUpWithEmailAndPassword] and [write] with correct arguments', 66 | () async { 67 | when( 68 | mockRemoteDataSource.signUpWithEmailAndPassword( 69 | email: email, 70 | password: password, 71 | ), 72 | ).thenAnswer((_) async => authUserModel); 73 | 74 | await authRepository.signUp(email: email, password: password); 75 | 76 | verify( 77 | mockRemoteDataSource.signUpWithEmailAndPassword( 78 | email: email, 79 | password: password, 80 | ), 81 | ).called(1); 82 | 83 | verify(mockLocalDataSource.write(key: 'user', value: authUserModel)) 84 | .called(1); 85 | }); 86 | 87 | test( 88 | 'returns an AuthUser when remoteDataSource.signUpWithEmailAndPassword returns an AuthUserModel successfully', 89 | () async { 90 | when( 91 | mockRemoteDataSource.signUpWithEmailAndPassword( 92 | email: email, 93 | password: password, 94 | ), 95 | ).thenAnswer((_) async => authUserModel); 96 | final results = 97 | await authRepository.signUp(email: email, password: password); 98 | 99 | expect(results, equals(authUserModel.toEntity())); 100 | }); 101 | }); 102 | 103 | group('signIn', () { 104 | test('calls signInWithEmailAndPassword with correct arguments', () async { 105 | when(mockRemoteDataSource.signInWithEmailAndPassword( 106 | email: email, 107 | password: password, 108 | )).thenAnswer((_) async => authUserModel); 109 | 110 | await authRepository.signIn(email: email, password: password); 111 | 112 | verify(mockRemoteDataSource.signInWithEmailAndPassword( 113 | email: email, 114 | password: password, 115 | )).called(1); 116 | 117 | verify(mockLocalDataSource.write(key: 'user', value: authUserModel)) 118 | .called(1); 119 | }); 120 | 121 | test( 122 | 'returns an AuthUser when remoteDataSource.signInWithEmailAndPassword returns an AuthUserModel successfully', 123 | () async { 124 | when( 125 | mockRemoteDataSource.signInWithEmailAndPassword( 126 | email: email, 127 | password: password, 128 | ), 129 | ).thenAnswer((_) async => authUserModel); 130 | 131 | final results = 132 | await authRepository.signIn(email: email, password: password); 133 | 134 | expect(results, equals(authUserModel.toEntity())); 135 | }); 136 | }); 137 | 138 | group('signOut', () { 139 | test('calls signOut and clears local user data', () async { 140 | when(mockRemoteDataSource.signOut()).thenAnswer((_) async {}); 141 | 142 | await authRepository.signOut(); 143 | 144 | verify(mockRemoteDataSource.signOut()).called(1); 145 | verify(mockLocalDataSource.write(key: 'user', value: null)).called(1); 146 | }); 147 | }); 148 | } 149 | -------------------------------------------------------------------------------- /test/src/features/auth/data/repositories/auth_repository_impl_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.4.0 from annotations 2 | // in flutter_clean_architecture_with_firebase/test/src/features/auth/data/repositories/auth_repository_impl_test.dart. 3 | // Do not manually edit this file. 4 | 5 | // ignore_for_file: no_leading_underscores_for_library_prefixes 6 | import 'dart:async' as _i4; 7 | 8 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/data/data_sources/auth_local_data_source.dart' 9 | as _i5; 10 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/data/data_sources/auth_remote_data_source.dart' 11 | as _i3; 12 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/data/models/auth_user_model.dart' 13 | as _i2; 14 | import 'package:mockito/mockito.dart' as _i1; 15 | 16 | // ignore_for_file: type=lint 17 | // ignore_for_file: avoid_redundant_argument_values 18 | // ignore_for_file: avoid_setters_without_getters 19 | // ignore_for_file: comment_references 20 | // ignore_for_file: implementation_imports 21 | // ignore_for_file: invalid_use_of_visible_for_testing_member 22 | // ignore_for_file: prefer_const_constructors 23 | // ignore_for_file: unnecessary_parenthesis 24 | // ignore_for_file: camel_case_types 25 | // ignore_for_file: subtype_of_sealed_class 26 | 27 | class _FakeAuthUserModel_0 extends _i1.SmartFake implements _i2.AuthUserModel { 28 | _FakeAuthUserModel_0( 29 | Object parent, 30 | Invocation parentInvocation, 31 | ) : super( 32 | parent, 33 | parentInvocation, 34 | ); 35 | } 36 | 37 | /// A class which mocks [AuthRemoteDataSource]. 38 | /// 39 | /// See the documentation for Mockito's code generation for more information. 40 | class MockAuthRemoteDataSource extends _i1.Mock 41 | implements _i3.AuthRemoteDataSource { 42 | MockAuthRemoteDataSource() { 43 | _i1.throwOnMissingStub(this); 44 | } 45 | 46 | @override 47 | _i4.Stream<_i2.AuthUserModel?> get user => (super.noSuchMethod( 48 | Invocation.getter(#user), 49 | returnValue: _i4.Stream<_i2.AuthUserModel?>.empty(), 50 | ) as _i4.Stream<_i2.AuthUserModel?>); 51 | @override 52 | _i4.Future<_i2.AuthUserModel> signUpWithEmailAndPassword({ 53 | required String? email, 54 | required String? password, 55 | }) => 56 | (super.noSuchMethod( 57 | Invocation.method( 58 | #signUpWithEmailAndPassword, 59 | [], 60 | { 61 | #email: email, 62 | #password: password, 63 | }, 64 | ), 65 | returnValue: _i4.Future<_i2.AuthUserModel>.value(_FakeAuthUserModel_0( 66 | this, 67 | Invocation.method( 68 | #signUpWithEmailAndPassword, 69 | [], 70 | { 71 | #email: email, 72 | #password: password, 73 | }, 74 | ), 75 | )), 76 | ) as _i4.Future<_i2.AuthUserModel>); 77 | @override 78 | _i4.Future<_i2.AuthUserModel> signInWithEmailAndPassword({ 79 | required String? email, 80 | required String? password, 81 | }) => 82 | (super.noSuchMethod( 83 | Invocation.method( 84 | #signInWithEmailAndPassword, 85 | [], 86 | { 87 | #email: email, 88 | #password: password, 89 | }, 90 | ), 91 | returnValue: _i4.Future<_i2.AuthUserModel>.value(_FakeAuthUserModel_0( 92 | this, 93 | Invocation.method( 94 | #signInWithEmailAndPassword, 95 | [], 96 | { 97 | #email: email, 98 | #password: password, 99 | }, 100 | ), 101 | )), 102 | ) as _i4.Future<_i2.AuthUserModel>); 103 | @override 104 | _i4.Future signOut() => (super.noSuchMethod( 105 | Invocation.method( 106 | #signOut, 107 | [], 108 | ), 109 | returnValue: _i4.Future.value(), 110 | returnValueForMissingStub: _i4.Future.value(), 111 | ) as _i4.Future); 112 | } 113 | 114 | /// A class which mocks [AuthLocalDataSource]. 115 | /// 116 | /// See the documentation for Mockito's code generation for more information. 117 | class MockAuthLocalDataSource extends _i1.Mock 118 | implements _i5.AuthLocalDataSource { 119 | MockAuthLocalDataSource() { 120 | _i1.throwOnMissingStub(this); 121 | } 122 | 123 | @override 124 | Map get authLocalDataSource => (super.noSuchMethod( 125 | Invocation.getter(#authLocalDataSource), 126 | returnValue: {}, 127 | ) as Map); 128 | @override 129 | void write({ 130 | required String? key, 131 | T? value, 132 | }) => 133 | super.noSuchMethod( 134 | Invocation.method( 135 | #write, 136 | [], 137 | { 138 | #key: key, 139 | #value: value, 140 | }, 141 | ), 142 | returnValueForMissingStub: null, 143 | ); 144 | @override 145 | T? read({required String? key}) => 146 | (super.noSuchMethod(Invocation.method( 147 | #read, 148 | [], 149 | {#key: key}, 150 | )) as T?); 151 | } 152 | -------------------------------------------------------------------------------- /test/src/features/auth/domain/entities/auth_user_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/entities/auth_user.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | test('empty AuthUser has correct default values', () { 6 | expect(AuthUser.empty.id, equals('')); 7 | expect(AuthUser.empty.email, equals('')); 8 | expect(AuthUser.empty.name, equals('')); 9 | expect(AuthUser.empty.photoURL, equals('')); 10 | }); 11 | 12 | test('two AuthUser instances with same values are equal', () { 13 | const user1 = AuthUser( 14 | id: 'id', 15 | email: 'email', 16 | name: 'name', 17 | photoURL: 'photoURL', 18 | ); 19 | const user2 = AuthUser( 20 | id: 'id', 21 | email: 'email', 22 | name: 'name', 23 | photoURL: 'photoURL', 24 | ); 25 | expect(user1, equals(user2)); 26 | }); 27 | 28 | test('two AuthUser instances with different values are not equal', () { 29 | const user1 = AuthUser( 30 | id: 'id1', 31 | email: 'email', 32 | name: 'name', 33 | photoURL: 'photoURL', 34 | ); 35 | const user2 = AuthUser( 36 | id: 'id2', 37 | email: 'email', 38 | name: 'name', 39 | photoURL: 'photoURL', 40 | ); 41 | expect(user1, isNot(equals(user2))); 42 | }); 43 | 44 | test('props returns correct properties', () { 45 | const user = AuthUser( 46 | id: 'id', 47 | email: 'email', 48 | name: 'name', 49 | photoURL: 'photoURL', 50 | ); 51 | expect(user.props, equals(['id', 'name', 'email', 'photoURL'])); 52 | }); 53 | 54 | test('name and photoURL can be null', () { 55 | const user = AuthUser(id: 'id', email: 'email'); 56 | expect(user.name, isNull); 57 | expect(user.photoURL, isNull); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /test/src/features/auth/domain/use_cases/sign_in_use_case_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/entities/auth_user.dart'; 2 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/repositories/auth_repository.dart'; 3 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/use_cases/sign_in_use_case.dart'; 4 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/value_objects/email.dart'; 5 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/value_objects/password.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:mockito/annotations.dart'; 8 | import 'package:mockito/mockito.dart'; 9 | 10 | import 'sign_in_use_case_test.mocks.dart'; 11 | 12 | @GenerateMocks([AuthRepository]) 13 | void main() { 14 | late SignInUseCase signInUseCase; 15 | late MockAuthRepository mockAuthRepository; 16 | 17 | setUp(() { 18 | mockAuthRepository = MockAuthRepository(); 19 | signInUseCase = SignInUseCase(authRepository: mockAuthRepository); 20 | }); 21 | 22 | final tEmail = Email((email) => email.value = 'test@test.com'); 23 | final tPassword = Password((password) => password.value = 'password123'); 24 | const tAuthUser = AuthUser(id: '123', email: 'test@test.com'); 25 | final tSignInParams = SignInParams(email: tEmail, password: tPassword); 26 | 27 | test( 28 | 'should call signIn method on the AuthRepository with correct parameters', 29 | () async { 30 | when(mockAuthRepository.signIn( 31 | email: anyNamed('email'), 32 | password: anyNamed('password'), 33 | )).thenAnswer((_) async => tAuthUser); 34 | 35 | await signInUseCase.call(tSignInParams); 36 | 37 | verify(mockAuthRepository.signIn( 38 | email: tEmail.value, 39 | password: tPassword.value, 40 | )); 41 | }, 42 | ); 43 | 44 | test( 45 | 'should throw an exception when the signIn method on the AuthRepository throws an exception', 46 | () async { 47 | when(mockAuthRepository.signIn( 48 | email: tEmail.value, 49 | password: tPassword.value, 50 | )).thenThrow(Exception()); 51 | 52 | final call = signInUseCase.call; 53 | 54 | expect(() => call(tSignInParams), throwsA(isInstanceOf())); 55 | }, 56 | ); 57 | 58 | test( 59 | 'should return the correct AuthUser when the signIn method on the AuthRepository returns an AuthUser', 60 | () async { 61 | when(mockAuthRepository.signIn( 62 | email: tEmail.value, 63 | password: tPassword.value, 64 | )).thenAnswer((_) async => tAuthUser); 65 | 66 | final result = await signInUseCase.call(tSignInParams); 67 | 68 | expect(result, equals(tAuthUser)); 69 | }, 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /test/src/features/auth/domain/use_cases/sign_in_use_case_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.4.0 from annotations 2 | // in flutter_clean_architecture_with_firebase/test/src/features/auth/domain/use_cases/sign_in_use_case_test.dart. 3 | // Do not manually edit this file. 4 | 5 | // ignore_for_file: no_leading_underscores_for_library_prefixes 6 | import 'dart:async' as _i4; 7 | 8 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/entities/auth_user.dart' 9 | as _i2; 10 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/repositories/auth_repository.dart' 11 | as _i3; 12 | import 'package:mockito/mockito.dart' as _i1; 13 | 14 | // ignore_for_file: type=lint 15 | // ignore_for_file: avoid_redundant_argument_values 16 | // ignore_for_file: avoid_setters_without_getters 17 | // ignore_for_file: comment_references 18 | // ignore_for_file: implementation_imports 19 | // ignore_for_file: invalid_use_of_visible_for_testing_member 20 | // ignore_for_file: prefer_const_constructors 21 | // ignore_for_file: unnecessary_parenthesis 22 | // ignore_for_file: camel_case_types 23 | // ignore_for_file: subtype_of_sealed_class 24 | 25 | class _FakeAuthUser_0 extends _i1.SmartFake implements _i2.AuthUser { 26 | _FakeAuthUser_0( 27 | Object parent, 28 | Invocation parentInvocation, 29 | ) : super( 30 | parent, 31 | parentInvocation, 32 | ); 33 | } 34 | 35 | /// A class which mocks [AuthRepository]. 36 | /// 37 | /// See the documentation for Mockito's code generation for more information. 38 | class MockAuthRepository extends _i1.Mock implements _i3.AuthRepository { 39 | MockAuthRepository() { 40 | _i1.throwOnMissingStub(this); 41 | } 42 | 43 | @override 44 | _i4.Stream<_i2.AuthUser> get authUser => (super.noSuchMethod( 45 | Invocation.getter(#authUser), 46 | returnValue: _i4.Stream<_i2.AuthUser>.empty(), 47 | ) as _i4.Stream<_i2.AuthUser>); 48 | @override 49 | _i4.Future<_i2.AuthUser> signUp({ 50 | required String? email, 51 | required String? password, 52 | }) => 53 | (super.noSuchMethod( 54 | Invocation.method( 55 | #signUp, 56 | [], 57 | { 58 | #email: email, 59 | #password: password, 60 | }, 61 | ), 62 | returnValue: _i4.Future<_i2.AuthUser>.value(_FakeAuthUser_0( 63 | this, 64 | Invocation.method( 65 | #signUp, 66 | [], 67 | { 68 | #email: email, 69 | #password: password, 70 | }, 71 | ), 72 | )), 73 | ) as _i4.Future<_i2.AuthUser>); 74 | @override 75 | _i4.Future<_i2.AuthUser> signIn({ 76 | required String? email, 77 | required String? password, 78 | }) => 79 | (super.noSuchMethod( 80 | Invocation.method( 81 | #signIn, 82 | [], 83 | { 84 | #email: email, 85 | #password: password, 86 | }, 87 | ), 88 | returnValue: _i4.Future<_i2.AuthUser>.value(_FakeAuthUser_0( 89 | this, 90 | Invocation.method( 91 | #signIn, 92 | [], 93 | { 94 | #email: email, 95 | #password: password, 96 | }, 97 | ), 98 | )), 99 | ) as _i4.Future<_i2.AuthUser>); 100 | @override 101 | _i4.Future signOut() => (super.noSuchMethod( 102 | Invocation.method( 103 | #signOut, 104 | [], 105 | ), 106 | returnValue: _i4.Future.value(), 107 | returnValueForMissingStub: _i4.Future.value(), 108 | ) as _i4.Future); 109 | } 110 | -------------------------------------------------------------------------------- /test/src/features/auth/domain/use_cases/sign_out_use_case_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/repositories/auth_repository.dart'; 2 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/use_cases/sign_out_use_case.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:mockito/annotations.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | 7 | import 'sign_in_use_case_test.mocks.dart'; 8 | 9 | @GenerateMocks([AuthRepository]) 10 | void main() { 11 | late MockAuthRepository mockAuthRepository; 12 | late SignOutUseCase signOutUseCase; 13 | 14 | setUp(() { 15 | mockAuthRepository = MockAuthRepository(); 16 | signOutUseCase = SignOutUseCase(authRepository: mockAuthRepository); 17 | }); 18 | 19 | test('should call signOut method on the AuthRepository', () async { 20 | when(mockAuthRepository.signOut()).thenAnswer((_) async => null); 21 | 22 | await signOutUseCase.call(); 23 | 24 | verify(mockAuthRepository.signOut()); 25 | }); 26 | 27 | test( 28 | 'should throw an exception when the signOut method on the AuthRepository throws an exception', 29 | () async { 30 | when(mockAuthRepository.signOut()).thenThrow(Exception()); 31 | 32 | expect(() async => await signOutUseCase.call(), throwsA(isA())); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /test/src/features/auth/domain/use_cases/sign_out_use_case_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.4.0 from annotations 2 | // in flutter_clean_architecture_with_firebase/test/src/features/auth/domain/use_cases/sign_out_use_case_test.dart. 3 | // Do not manually edit this file. 4 | 5 | // ignore_for_file: no_leading_underscores_for_library_prefixes 6 | import 'dart:async' as _i4; 7 | 8 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/entities/auth_user.dart' 9 | as _i2; 10 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/repositories/auth_repository.dart' 11 | as _i3; 12 | import 'package:mockito/mockito.dart' as _i1; 13 | 14 | // ignore_for_file: type=lint 15 | // ignore_for_file: avoid_redundant_argument_values 16 | // ignore_for_file: avoid_setters_without_getters 17 | // ignore_for_file: comment_references 18 | // ignore_for_file: implementation_imports 19 | // ignore_for_file: invalid_use_of_visible_for_testing_member 20 | // ignore_for_file: prefer_const_constructors 21 | // ignore_for_file: unnecessary_parenthesis 22 | // ignore_for_file: camel_case_types 23 | // ignore_for_file: subtype_of_sealed_class 24 | 25 | class _FakeAuthUser_0 extends _i1.SmartFake implements _i2.AuthUser { 26 | _FakeAuthUser_0( 27 | Object parent, 28 | Invocation parentInvocation, 29 | ) : super( 30 | parent, 31 | parentInvocation, 32 | ); 33 | } 34 | 35 | /// A class which mocks [AuthRepository]. 36 | /// 37 | /// See the documentation for Mockito's code generation for more information. 38 | class MockAuthRepository extends _i1.Mock implements _i3.AuthRepository { 39 | MockAuthRepository() { 40 | _i1.throwOnMissingStub(this); 41 | } 42 | 43 | @override 44 | _i4.Stream<_i2.AuthUser> get authUser => (super.noSuchMethod( 45 | Invocation.getter(#authUser), 46 | returnValue: _i4.Stream<_i2.AuthUser>.empty(), 47 | ) as _i4.Stream<_i2.AuthUser>); 48 | @override 49 | _i4.Future<_i2.AuthUser> signUp({ 50 | required String? email, 51 | required String? password, 52 | }) => 53 | (super.noSuchMethod( 54 | Invocation.method( 55 | #signUp, 56 | [], 57 | { 58 | #email: email, 59 | #password: password, 60 | }, 61 | ), 62 | returnValue: _i4.Future<_i2.AuthUser>.value(_FakeAuthUser_0( 63 | this, 64 | Invocation.method( 65 | #signUp, 66 | [], 67 | { 68 | #email: email, 69 | #password: password, 70 | }, 71 | ), 72 | )), 73 | ) as _i4.Future<_i2.AuthUser>); 74 | @override 75 | _i4.Future<_i2.AuthUser> signIn({ 76 | required String? email, 77 | required String? password, 78 | }) => 79 | (super.noSuchMethod( 80 | Invocation.method( 81 | #signIn, 82 | [], 83 | { 84 | #email: email, 85 | #password: password, 86 | }, 87 | ), 88 | returnValue: _i4.Future<_i2.AuthUser>.value(_FakeAuthUser_0( 89 | this, 90 | Invocation.method( 91 | #signIn, 92 | [], 93 | { 94 | #email: email, 95 | #password: password, 96 | }, 97 | ), 98 | )), 99 | ) as _i4.Future<_i2.AuthUser>); 100 | @override 101 | _i4.Future signOut() => (super.noSuchMethod( 102 | Invocation.method( 103 | #signOut, 104 | [], 105 | ), 106 | returnValue: _i4.Future.value(), 107 | returnValueForMissingStub: _i4.Future.value(), 108 | ) as _i4.Future); 109 | } 110 | -------------------------------------------------------------------------------- /test/src/features/auth/domain/use_cases/sign_up_use_case_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/entities/auth_user.dart'; 2 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/repositories/auth_repository.dart'; 3 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/use_cases/sign_up_use_case.dart'; 4 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/value_objects/email.dart'; 5 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/value_objects/password.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:mockito/annotations.dart'; 8 | import 'package:mockito/mockito.dart'; 9 | 10 | import 'sign_in_use_case_test.mocks.dart'; 11 | 12 | @GenerateMocks([AuthRepository]) 13 | void main() { 14 | late MockAuthRepository mockAuthRepository; 15 | late SignUpUseCase signUpUseCase; 16 | 17 | setUp(() { 18 | mockAuthRepository = MockAuthRepository(); 19 | signUpUseCase = SignUpUseCase(authRepository: mockAuthRepository); 20 | }); 21 | 22 | final tEmail = Email((email) => email.value = 'test@test.com'); 23 | final tPassword = Password((password) => password.value = 'password123'); 24 | const tAuthUser = AuthUser(id: '123', email: 'test@test.com'); 25 | final tSignUpParams = SignUpParams(email: tEmail, password: tPassword); 26 | 27 | test( 28 | 'should call signUp method on the AuthRepository with correct parameters', 29 | () async { 30 | when(mockAuthRepository.signUp( 31 | email: anyNamed('email'), 32 | password: anyNamed('password'), 33 | )).thenAnswer((_) async => tAuthUser); 34 | 35 | await signUpUseCase.call(tSignUpParams); 36 | 37 | verify(mockAuthRepository.signUp( 38 | email: tSignUpParams.email.value, 39 | password: tSignUpParams.password.value, 40 | )); 41 | }); 42 | 43 | test( 44 | 'should throw an exception when the signUp method on the AuthRepository throws an exception', 45 | () async { 46 | when(mockAuthRepository.signUp( 47 | email: anyNamed('email'), 48 | password: anyNamed('password'), 49 | )).thenThrow(Exception()); 50 | 51 | expect(() async => await signUpUseCase.call(tSignUpParams), 52 | throwsA(isA())); 53 | }); 54 | 55 | test( 56 | 'should return the correct AuthUser when the signUp method on the AuthRepository returns an AuthUser', 57 | () async { 58 | when(mockAuthRepository.signUp( 59 | email: anyNamed('email'), 60 | password: anyNamed('password'), 61 | )).thenAnswer((_) async => tAuthUser); 62 | 63 | final result = await signUpUseCase.call(tSignUpParams); 64 | 65 | expect(result, equals(tAuthUser)); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /test/src/features/auth/domain/use_cases/sign_up_use_case_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.4.0 from annotations 2 | // in flutter_clean_architecture_with_firebase/test/src/features/auth/domain/use_cases/sign_up_use_case_test.dart. 3 | // Do not manually edit this file. 4 | 5 | // ignore_for_file: no_leading_underscores_for_library_prefixes 6 | import 'dart:async' as _i4; 7 | 8 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/entities/auth_user.dart' 9 | as _i2; 10 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/repositories/auth_repository.dart' 11 | as _i3; 12 | import 'package:mockito/mockito.dart' as _i1; 13 | 14 | // ignore_for_file: type=lint 15 | // ignore_for_file: avoid_redundant_argument_values 16 | // ignore_for_file: avoid_setters_without_getters 17 | // ignore_for_file: comment_references 18 | // ignore_for_file: implementation_imports 19 | // ignore_for_file: invalid_use_of_visible_for_testing_member 20 | // ignore_for_file: prefer_const_constructors 21 | // ignore_for_file: unnecessary_parenthesis 22 | // ignore_for_file: camel_case_types 23 | // ignore_for_file: subtype_of_sealed_class 24 | 25 | class _FakeAuthUser_0 extends _i1.SmartFake implements _i2.AuthUser { 26 | _FakeAuthUser_0( 27 | Object parent, 28 | Invocation parentInvocation, 29 | ) : super( 30 | parent, 31 | parentInvocation, 32 | ); 33 | } 34 | 35 | /// A class which mocks [AuthRepository]. 36 | /// 37 | /// See the documentation for Mockito's code generation for more information. 38 | class MockAuthRepository extends _i1.Mock implements _i3.AuthRepository { 39 | MockAuthRepository() { 40 | _i1.throwOnMissingStub(this); 41 | } 42 | 43 | @override 44 | _i4.Stream<_i2.AuthUser> get authUser => (super.noSuchMethod( 45 | Invocation.getter(#authUser), 46 | returnValue: _i4.Stream<_i2.AuthUser>.empty(), 47 | ) as _i4.Stream<_i2.AuthUser>); 48 | @override 49 | _i4.Future<_i2.AuthUser> signUp({ 50 | required String? email, 51 | required String? password, 52 | }) => 53 | (super.noSuchMethod( 54 | Invocation.method( 55 | #signUp, 56 | [], 57 | { 58 | #email: email, 59 | #password: password, 60 | }, 61 | ), 62 | returnValue: _i4.Future<_i2.AuthUser>.value(_FakeAuthUser_0( 63 | this, 64 | Invocation.method( 65 | #signUp, 66 | [], 67 | { 68 | #email: email, 69 | #password: password, 70 | }, 71 | ), 72 | )), 73 | ) as _i4.Future<_i2.AuthUser>); 74 | @override 75 | _i4.Future<_i2.AuthUser> signIn({ 76 | required String? email, 77 | required String? password, 78 | }) => 79 | (super.noSuchMethod( 80 | Invocation.method( 81 | #signIn, 82 | [], 83 | { 84 | #email: email, 85 | #password: password, 86 | }, 87 | ), 88 | returnValue: _i4.Future<_i2.AuthUser>.value(_FakeAuthUser_0( 89 | this, 90 | Invocation.method( 91 | #signIn, 92 | [], 93 | { 94 | #email: email, 95 | #password: password, 96 | }, 97 | ), 98 | )), 99 | ) as _i4.Future<_i2.AuthUser>); 100 | @override 101 | _i4.Future signOut() => (super.noSuchMethod( 102 | Invocation.method( 103 | #signOut, 104 | [], 105 | ), 106 | returnValue: _i4.Future.value(), 107 | returnValueForMissingStub: _i4.Future.value(), 108 | ) as _i4.Future); 109 | } 110 | -------------------------------------------------------------------------------- /test/src/features/auth/domain/use_cases/stream_auth_user_use_case_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/entities/auth_user.dart'; 2 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/repositories/auth_repository.dart'; 3 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/use_cases/stream_auth_user_use_case.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:mockito/annotations.dart'; 6 | import 'package:mockito/mockito.dart'; 7 | 8 | import 'stream_auth_user_use_case_test.mocks.dart'; 9 | 10 | @GenerateMocks([AuthRepository]) 11 | void main() { 12 | late StreamAuthUserUseCase streamAuthUserUseCase; 13 | late MockAuthRepository mockAuthRepository; 14 | 15 | setUp(() { 16 | mockAuthRepository = MockAuthRepository(); 17 | streamAuthUserUseCase = 18 | StreamAuthUserUseCase(authRepository: mockAuthRepository); 19 | }); 20 | 21 | const tAuthUser = AuthUser(id: '123', email: 'test@test.com'); 22 | 23 | test( 24 | 'should call authUser getter on the AuthRepository', 25 | () async { 26 | when(mockAuthRepository.authUser).thenAnswer( 27 | (_) => Stream.value(tAuthUser), 28 | ); 29 | 30 | streamAuthUserUseCase.call(); 31 | 32 | verify(mockAuthRepository.authUser); 33 | }, 34 | ); 35 | 36 | test( 37 | 'should throw an exception when the authUser getter on the AuthRepository throws an exception', 38 | () async { 39 | when(mockAuthRepository.authUser).thenThrow(Exception()); 40 | 41 | final call = streamAuthUserUseCase.call; 42 | 43 | expect(() => call(), throwsA(isInstanceOf())); 44 | }, 45 | ); 46 | 47 | test( 48 | 'should return the correct AuthUser when the authUser getter on the AuthRepository returns an AuthUser', 49 | () async { 50 | when(mockAuthRepository.authUser) 51 | .thenAnswer((_) => Stream.value(tAuthUser)); 52 | 53 | final result = streamAuthUserUseCase.call(); 54 | final authUser = await result.first; 55 | expect(authUser, equals(tAuthUser)); 56 | }, 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /test/src/features/auth/domain/use_cases/stream_auth_user_use_case_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.4.0 from annotations 2 | // in flutter_clean_architecture_with_firebase/test/src/features/auth/domain/use_cases/stream_auth_user_use_case_test.dart. 3 | // Do not manually edit this file. 4 | 5 | // ignore_for_file: no_leading_underscores_for_library_prefixes 6 | import 'dart:async' as _i4; 7 | 8 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/entities/auth_user.dart' 9 | as _i2; 10 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/repositories/auth_repository.dart' 11 | as _i3; 12 | import 'package:mockito/mockito.dart' as _i1; 13 | 14 | // ignore_for_file: type=lint 15 | // ignore_for_file: avoid_redundant_argument_values 16 | // ignore_for_file: avoid_setters_without_getters 17 | // ignore_for_file: comment_references 18 | // ignore_for_file: implementation_imports 19 | // ignore_for_file: invalid_use_of_visible_for_testing_member 20 | // ignore_for_file: prefer_const_constructors 21 | // ignore_for_file: unnecessary_parenthesis 22 | // ignore_for_file: camel_case_types 23 | // ignore_for_file: subtype_of_sealed_class 24 | 25 | class _FakeAuthUser_0 extends _i1.SmartFake implements _i2.AuthUser { 26 | _FakeAuthUser_0( 27 | Object parent, 28 | Invocation parentInvocation, 29 | ) : super( 30 | parent, 31 | parentInvocation, 32 | ); 33 | } 34 | 35 | /// A class which mocks [AuthRepository]. 36 | /// 37 | /// See the documentation for Mockito's code generation for more information. 38 | class MockAuthRepository extends _i1.Mock implements _i3.AuthRepository { 39 | MockAuthRepository() { 40 | _i1.throwOnMissingStub(this); 41 | } 42 | 43 | @override 44 | _i4.Stream<_i2.AuthUser> get authUser => (super.noSuchMethod( 45 | Invocation.getter(#authUser), 46 | returnValue: _i4.Stream<_i2.AuthUser>.empty(), 47 | ) as _i4.Stream<_i2.AuthUser>); 48 | @override 49 | _i4.Future<_i2.AuthUser> signUp({ 50 | required String? email, 51 | required String? password, 52 | }) => 53 | (super.noSuchMethod( 54 | Invocation.method( 55 | #signUp, 56 | [], 57 | { 58 | #email: email, 59 | #password: password, 60 | }, 61 | ), 62 | returnValue: _i4.Future<_i2.AuthUser>.value(_FakeAuthUser_0( 63 | this, 64 | Invocation.method( 65 | #signUp, 66 | [], 67 | { 68 | #email: email, 69 | #password: password, 70 | }, 71 | ), 72 | )), 73 | ) as _i4.Future<_i2.AuthUser>); 74 | @override 75 | _i4.Future<_i2.AuthUser> signIn({ 76 | required String? email, 77 | required String? password, 78 | }) => 79 | (super.noSuchMethod( 80 | Invocation.method( 81 | #signIn, 82 | [], 83 | { 84 | #email: email, 85 | #password: password, 86 | }, 87 | ), 88 | returnValue: _i4.Future<_i2.AuthUser>.value(_FakeAuthUser_0( 89 | this, 90 | Invocation.method( 91 | #signIn, 92 | [], 93 | { 94 | #email: email, 95 | #password: password, 96 | }, 97 | ), 98 | )), 99 | ) as _i4.Future<_i2.AuthUser>); 100 | @override 101 | _i4.Future signOut() => (super.noSuchMethod( 102 | Invocation.method( 103 | #signOut, 104 | [], 105 | ), 106 | returnValue: _i4.Future.value(), 107 | returnValueForMissingStub: _i4.Future.value(), 108 | ) as _i4.Future); 109 | } 110 | -------------------------------------------------------------------------------- /test/src/features/auth/domain/value_objects/email_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/value_objects/email.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | group('Email', () { 6 | test('valid email does not throw exception', () { 7 | expect(() => Email((b) => b.value = 'test@test.com'), returnsNormally); 8 | }); 9 | 10 | test('invalid email throws ArgumentError', () { 11 | expect(() => Email((b) => b.value = 'invalidEmail'), throwsArgumentError); 12 | }); 13 | 14 | test('value property is correctly set', () { 15 | final email = Email((b) => b.value = 'test@test.com'); 16 | expect(email.value, equals('test@test.com')); 17 | }); 18 | 19 | test('two Email instances with same values are equal', () { 20 | final email1 = Email((b) => b.value = 'test@test.com'); 21 | final email2 = Email((b) => b.value = 'test@test.com'); 22 | expect(email1, equals(email2)); 23 | }); 24 | 25 | test('two Email instances with different values are not equal', () { 26 | final email1 = Email((b) => b.value = 'test1@test.com'); 27 | final email2 = Email((b) => b.value = 'test2@test.com'); 28 | expect(email1, isNot(equals(email2))); 29 | }); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /test/src/features/auth/domain/value_objects/password_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/value_objects/password.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | group('Password', () { 6 | test('valid password does not throw exception', () { 7 | expect(() => Password((b) => b.value = 'password123'), returnsNormally); 8 | }); 9 | 10 | test('invalid password throws ArgumentError', () { 11 | expect(() => Password((b) => b.value = 'pass'), throwsArgumentError); 12 | expect(() => Password((b) => b.value = 'password!'), throwsArgumentError); 13 | }); 14 | 15 | test('value property is correctly set', () { 16 | final password = Password((b) => b.value = 'password123'); 17 | expect(password.value, equals('password123')); 18 | }); 19 | 20 | test('two Password instances with same values are equal', () { 21 | final password1 = Password((b) => b.value = 'password123'); 22 | final password2 = Password((b) => b.value = 'password123'); 23 | expect(password1, equals(password2)); 24 | }); 25 | 26 | test('two Password instances with different values are not equal', () { 27 | final password1 = Password((b) => b.value = 'password123'); 28 | final password2 = Password((b) => b.value = 'password456'); 29 | expect(password1, isNot(equals(password2))); 30 | }); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/src/features/auth/presentation/blocs/sign_in/sign_in_cubit_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc_test/bloc_test.dart'; 2 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/entities/auth_user.dart'; 3 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/use_cases/sign_in_use_case.dart'; 4 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/value_objects/email.dart'; 5 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/value_objects/password.dart'; 6 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/email_status.dart'; 7 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/form_status.dart'; 8 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/password_status.dart'; 9 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/sign_in/sign_in_cubit.dart'; 10 | import 'package:flutter_test/flutter_test.dart'; 11 | import 'package:mockito/annotations.dart'; 12 | import 'package:mockito/mockito.dart'; 13 | 14 | import 'sign_in_cubit_test.mocks.dart'; 15 | 16 | @GenerateMocks([SignInUseCase]) 17 | void main() { 18 | late MockSignInUseCase mockSignInUseCase; 19 | 20 | setUp(() { 21 | mockSignInUseCase = MockSignInUseCase(); 22 | }); 23 | 24 | group('SignInCubit', () { 25 | blocTest( 26 | 'emits [] when nothing is added', 27 | build: () => SignInCubit(signInUseCase: mockSignInUseCase), 28 | expect: () => [], 29 | ); 30 | 31 | blocTest( 32 | 'emits [valid email state] when valid email is added', 33 | build: () => SignInCubit(signInUseCase: mockSignInUseCase), 34 | act: (cubit) => cubit.emailChanged('test@test.com'), 35 | expect: () => [ 36 | SignInState( 37 | email: Email((e) => e..value = 'test@test.com'), 38 | emailStatus: EmailStatus.valid, 39 | ), 40 | ], 41 | ); 42 | 43 | blocTest( 44 | 'emits [invalid email state] when invalid email is added', 45 | build: () => SignInCubit(signInUseCase: mockSignInUseCase), 46 | act: (cubit) => cubit.emailChanged('invalid_email'), 47 | expect: () => [ 48 | const SignInState(emailStatus: EmailStatus.invalid), 49 | ], 50 | ); 51 | 52 | blocTest( 53 | 'emits [valid password state] when valid password is added', 54 | build: () => SignInCubit(signInUseCase: mockSignInUseCase), 55 | act: (cubit) => cubit.passwordChanged('password'), 56 | expect: () => [ 57 | SignInState( 58 | password: Password((p) => p..value = 'password'), 59 | passwordStatus: PasswordStatus.valid, 60 | ), 61 | ], 62 | ); 63 | 64 | blocTest( 65 | 'emits [invalid password state] when invalid password is added', 66 | build: () => SignInCubit(signInUseCase: mockSignInUseCase), 67 | act: (cubit) => cubit.passwordChanged('pass'), 68 | expect: () => [ 69 | const SignInState(passwordStatus: PasswordStatus.invalid), 70 | ], 71 | ); 72 | 73 | blocTest( 74 | 'emits formStatus [invalid, initial] when the form is not validated', 75 | build: () => SignInCubit(signInUseCase: mockSignInUseCase), 76 | seed: () => const SignInState( 77 | passwordStatus: PasswordStatus.unknown, 78 | emailStatus: EmailStatus.unknown, 79 | ), 80 | act: (cubit) => cubit.signIn(), 81 | expect: () => const [ 82 | SignInState( 83 | passwordStatus: PasswordStatus.unknown, 84 | emailStatus: EmailStatus.unknown, 85 | formStatus: FormStatus.invalid, 86 | ), 87 | SignInState( 88 | passwordStatus: PasswordStatus.unknown, 89 | emailStatus: EmailStatus.unknown, 90 | formStatus: FormStatus.initial, 91 | ), 92 | ], 93 | ); 94 | 95 | blocTest( 96 | 'emits [submissionInProgress, submissionSuccess] when signIn is successful', 97 | setUp: () { 98 | when(mockSignInUseCase(any)).thenAnswer( 99 | (_) => Future.value( 100 | const AuthUser(id: 'id', email: 'test@test.com'), 101 | ), 102 | ); 103 | }, 104 | build: () => SignInCubit(signInUseCase: mockSignInUseCase), 105 | seed: () => SignInState( 106 | email: Email((e) => e..value = 'test@test.com'), 107 | password: Password((p) => p..value = 'password123'), 108 | passwordStatus: PasswordStatus.valid, 109 | emailStatus: EmailStatus.valid, 110 | ), 111 | act: (cubit) => cubit.signIn(), 112 | expect: () => [ 113 | SignInState( 114 | email: Email((e) => e..value = 'test@test.com'), 115 | password: Password((p) => p..value = 'password123'), 116 | passwordStatus: PasswordStatus.valid, 117 | emailStatus: EmailStatus.valid, 118 | formStatus: FormStatus.submissionInProgress, 119 | ), 120 | SignInState( 121 | email: Email((e) => e..value = 'test@test.com'), 122 | password: Password((p) => p..value = 'password123'), 123 | passwordStatus: PasswordStatus.valid, 124 | emailStatus: EmailStatus.valid, 125 | formStatus: FormStatus.submissionSuccess, 126 | ), 127 | ], 128 | verify: (bloc) { 129 | verify(mockSignInUseCase(any)).called(1); 130 | }, 131 | ); 132 | }); 133 | } 134 | -------------------------------------------------------------------------------- /test/src/features/auth/presentation/blocs/sign_in/sign_in_cubit_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.4.0 from annotations 2 | // in flutter_clean_architecture_with_firebase/test/src/features/auth/presentation/blocs/sign_in/sign_in_cubit_test.dart. 3 | // Do not manually edit this file. 4 | 5 | // ignore_for_file: no_leading_underscores_for_library_prefixes 6 | import 'dart:async' as _i5; 7 | 8 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/entities/auth_user.dart' 9 | as _i3; 10 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/repositories/auth_repository.dart' 11 | as _i2; 12 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/use_cases/sign_in_use_case.dart' 13 | as _i4; 14 | import 'package:mockito/mockito.dart' as _i1; 15 | 16 | // ignore_for_file: type=lint 17 | // ignore_for_file: avoid_redundant_argument_values 18 | // ignore_for_file: avoid_setters_without_getters 19 | // ignore_for_file: comment_references 20 | // ignore_for_file: implementation_imports 21 | // ignore_for_file: invalid_use_of_visible_for_testing_member 22 | // ignore_for_file: prefer_const_constructors 23 | // ignore_for_file: unnecessary_parenthesis 24 | // ignore_for_file: camel_case_types 25 | // ignore_for_file: subtype_of_sealed_class 26 | 27 | class _FakeAuthRepository_0 extends _i1.SmartFake 28 | implements _i2.AuthRepository { 29 | _FakeAuthRepository_0( 30 | Object parent, 31 | Invocation parentInvocation, 32 | ) : super( 33 | parent, 34 | parentInvocation, 35 | ); 36 | } 37 | 38 | class _FakeAuthUser_1 extends _i1.SmartFake implements _i3.AuthUser { 39 | _FakeAuthUser_1( 40 | Object parent, 41 | Invocation parentInvocation, 42 | ) : super( 43 | parent, 44 | parentInvocation, 45 | ); 46 | } 47 | 48 | /// A class which mocks [SignInUseCase]. 49 | /// 50 | /// See the documentation for Mockito's code generation for more information. 51 | class MockSignInUseCase extends _i1.Mock implements _i4.SignInUseCase { 52 | MockSignInUseCase() { 53 | _i1.throwOnMissingStub(this); 54 | } 55 | 56 | @override 57 | _i2.AuthRepository get authRepository => (super.noSuchMethod( 58 | Invocation.getter(#authRepository), 59 | returnValue: _FakeAuthRepository_0( 60 | this, 61 | Invocation.getter(#authRepository), 62 | ), 63 | ) as _i2.AuthRepository); 64 | @override 65 | _i5.Future<_i3.AuthUser> call(_i4.SignInParams? params) => 66 | (super.noSuchMethod( 67 | Invocation.method( 68 | #call, 69 | [params], 70 | ), 71 | returnValue: _i5.Future<_i3.AuthUser>.value(_FakeAuthUser_1( 72 | this, 73 | Invocation.method( 74 | #call, 75 | [params], 76 | ), 77 | )), 78 | ) as _i5.Future<_i3.AuthUser>); 79 | } 80 | -------------------------------------------------------------------------------- /test/src/features/auth/presentation/blocs/sign_in/sign_in_state_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/value_objects/email.dart'; 2 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/value_objects/password.dart'; 3 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/email_status.dart'; 4 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/form_status.dart'; 5 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/password_status.dart'; 6 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/sign_in/sign_in_cubit.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | 9 | void main() { 10 | group('SignInState', () { 11 | test('should correctly copy state with new email', () { 12 | const initialState = SignInState(); 13 | final email = Email((email) => email..value = 'test@test.com'); 14 | 15 | final newState = initialState.copyWith( 16 | email: email, 17 | emailStatus: EmailStatus.valid, 18 | ); 19 | 20 | expect(newState.email, equals(email)); 21 | expect(newState.emailStatus, equals(EmailStatus.valid)); 22 | expect(newState.password, equals(initialState.password)); 23 | expect(newState.passwordStatus, equals(PasswordStatus.unknown)); 24 | }); 25 | 26 | test('should not update the email if the new value is invalid', () { 27 | const initialState = SignInState(); 28 | 29 | try { 30 | final email = Email((email) => email..value = 'testtest'); 31 | // ignore: unused_local_variable 32 | final newState = initialState.copyWith(email: email); 33 | 34 | // If the above line did not throw an error, fail the test 35 | fail('Should have thrown an ArgumentError'); 36 | } on ArgumentError { 37 | // If an ArgumentError was thrown, continue the test 38 | expect(initialState.email, isNull); 39 | } 40 | }); 41 | 42 | test('should correctly copy state with new password', () { 43 | const initialState = SignInState(); 44 | final password = Password((password) => password..value = 'password1234'); 45 | 46 | final newState = initialState.copyWith( 47 | password: password, 48 | passwordStatus: PasswordStatus.valid, 49 | ); 50 | 51 | expect(newState.password, equals(password)); 52 | expect(newState.passwordStatus, equals(PasswordStatus.valid)); 53 | expect(newState.email, equals(initialState.email)); 54 | expect(newState.emailStatus, equals(EmailStatus.unknown)); 55 | }); 56 | 57 | test('should not update the password if the new value is invalid', () { 58 | const initialState = SignInState(); 59 | 60 | try { 61 | final password = Password((password) => password..value = 'test'); 62 | // ignore: unused_local_variable 63 | final newState = initialState.copyWith(password: password); 64 | 65 | // If the above line did not throw an error, fail the test 66 | fail('Should have thrown an ArgumentError'); 67 | } on ArgumentError { 68 | // If an ArgumentError was thrown, continue the test 69 | expect(initialState.password, isNull); 70 | } 71 | }); 72 | 73 | test('should correctly copy state with new formStatus', () { 74 | const initialState = SignInState(); 75 | final newState = initialState.copyWith( 76 | formStatus: FormStatus.submissionInProgress, 77 | ); 78 | 79 | expect( 80 | newState.formStatus, 81 | equals(FormStatus.submissionInProgress), 82 | ); 83 | 84 | // The other fields do not change 85 | expect(newState.email, equals(initialState.email)); 86 | expect(newState.password, equals(initialState.password)); 87 | expect(newState.emailStatus, equals(initialState.emailStatus)); 88 | expect(newState.passwordStatus, equals(initialState.passwordStatus)); 89 | }); 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /test/src/features/auth/presentation/blocs/sign_up/sign_up_cubit_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc_test/bloc_test.dart'; 2 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/entities/auth_user.dart'; 3 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/use_cases/sign_up_use_case.dart'; 4 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/value_objects/email.dart'; 5 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/value_objects/password.dart'; 6 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/email_status.dart'; 7 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/form_status.dart'; 8 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/password_status.dart'; 9 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/sign_up/sign_up_cubit.dart'; 10 | import 'package:flutter_test/flutter_test.dart'; 11 | import 'package:mockito/annotations.dart'; 12 | import 'package:mockito/mockito.dart'; 13 | 14 | import 'sign_up_cubit_test.mocks.dart'; 15 | 16 | @GenerateMocks([SignUpUseCase]) 17 | void main() { 18 | late MockSignUpUseCase mockSignUpUseCase; 19 | 20 | setUp(() { 21 | mockSignUpUseCase = MockSignUpUseCase(); 22 | }); 23 | 24 | group('SignUpCubit', () { 25 | blocTest( 26 | 'emits [] when nothing is added', 27 | build: () => SignUpCubit(signUpUseCase: mockSignUpUseCase), 28 | expect: () => [], 29 | ); 30 | 31 | blocTest( 32 | 'emits [valid email state] when valid email is added', 33 | build: () => SignUpCubit(signUpUseCase: mockSignUpUseCase), 34 | act: (cubit) => cubit.emailChanged('test@test.com'), 35 | expect: () => [ 36 | SignUpState( 37 | email: Email((e) => e..value = 'test@test.com'), 38 | emailStatus: EmailStatus.valid, 39 | ), 40 | ], 41 | ); 42 | 43 | blocTest( 44 | 'emits [invalid email state] when invalid email is added', 45 | build: () => SignUpCubit(signUpUseCase: mockSignUpUseCase), 46 | act: (cubit) => cubit.emailChanged('invalid_email'), 47 | expect: () => [ 48 | const SignUpState(emailStatus: EmailStatus.invalid), 49 | ], 50 | ); 51 | 52 | blocTest( 53 | 'emits [valid password state] when valid password is added', 54 | build: () => SignUpCubit(signUpUseCase: mockSignUpUseCase), 55 | act: (cubit) => cubit.passwordChanged('password'), 56 | expect: () => [ 57 | SignUpState( 58 | password: Password((p) => p..value = 'password'), 59 | passwordStatus: PasswordStatus.valid, 60 | ), 61 | ], 62 | ); 63 | 64 | blocTest( 65 | 'emits [invalid password state] when invalid password is added', 66 | build: () => SignUpCubit(signUpUseCase: mockSignUpUseCase), 67 | act: (cubit) => cubit.passwordChanged('pass'), 68 | expect: () => [ 69 | const SignUpState(passwordStatus: PasswordStatus.invalid), 70 | ], 71 | ); 72 | 73 | blocTest( 74 | 'emits formStatus [invalid, initial] when the form is not validated', 75 | build: () => SignUpCubit(signUpUseCase: mockSignUpUseCase), 76 | seed: () => const SignUpState( 77 | passwordStatus: PasswordStatus.unknown, 78 | emailStatus: EmailStatus.unknown, 79 | ), 80 | act: (cubit) => cubit.signUp(), 81 | expect: () => const [ 82 | SignUpState( 83 | passwordStatus: PasswordStatus.unknown, 84 | emailStatus: EmailStatus.unknown, 85 | formStatus: FormStatus.invalid, 86 | ), 87 | SignUpState( 88 | passwordStatus: PasswordStatus.unknown, 89 | emailStatus: EmailStatus.unknown, 90 | formStatus: FormStatus.initial, 91 | ), 92 | ], 93 | ); 94 | 95 | blocTest( 96 | 'emits [submissionInProgress, submissionSuccess] when signUp is successful', 97 | setUp: () { 98 | when(mockSignUpUseCase(any)).thenAnswer( 99 | (_) => Future.value( 100 | const AuthUser(id: 'id', email: 'test@test.com'), 101 | ), 102 | ); 103 | }, 104 | build: () => SignUpCubit(signUpUseCase: mockSignUpUseCase), 105 | seed: () => SignUpState( 106 | email: Email((e) => e..value = 'test@test.com'), 107 | password: Password((p) => p..value = 'password123'), 108 | passwordStatus: PasswordStatus.valid, 109 | emailStatus: EmailStatus.valid, 110 | ), 111 | act: (cubit) => cubit.signUp(), 112 | expect: () => [ 113 | SignUpState( 114 | email: Email((e) => e..value = 'test@test.com'), 115 | password: Password((p) => p..value = 'password123'), 116 | passwordStatus: PasswordStatus.valid, 117 | emailStatus: EmailStatus.valid, 118 | formStatus: FormStatus.submissionInProgress, 119 | ), 120 | SignUpState( 121 | email: Email((e) => e..value = 'test@test.com'), 122 | password: Password((p) => p..value = 'password123'), 123 | passwordStatus: PasswordStatus.valid, 124 | emailStatus: EmailStatus.valid, 125 | formStatus: FormStatus.submissionSuccess, 126 | ), 127 | ], 128 | verify: (bloc) { 129 | verify(mockSignUpUseCase(any)).called(1); 130 | }, 131 | ); 132 | }); 133 | } 134 | -------------------------------------------------------------------------------- /test/src/features/auth/presentation/blocs/sign_up/sign_up_cubit_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.4.0 from annotations 2 | // in flutter_clean_architecture_with_firebase/test/src/features/auth/presentation/blocs/sign_up/sign_up_cubit_test.dart. 3 | // Do not manually edit this file. 4 | 5 | // ignore_for_file: no_leading_underscores_for_library_prefixes 6 | import 'dart:async' as _i5; 7 | 8 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/entities/auth_user.dart' 9 | as _i3; 10 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/repositories/auth_repository.dart' 11 | as _i2; 12 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/use_cases/sign_up_use_case.dart' 13 | as _i4; 14 | import 'package:mockito/mockito.dart' as _i1; 15 | 16 | // ignore_for_file: type=lint 17 | // ignore_for_file: avoid_redundant_argument_values 18 | // ignore_for_file: avoid_setters_without_getters 19 | // ignore_for_file: comment_references 20 | // ignore_for_file: implementation_imports 21 | // ignore_for_file: invalid_use_of_visible_for_testing_member 22 | // ignore_for_file: prefer_const_constructors 23 | // ignore_for_file: unnecessary_parenthesis 24 | // ignore_for_file: camel_case_types 25 | // ignore_for_file: subtype_of_sealed_class 26 | 27 | class _FakeAuthRepository_0 extends _i1.SmartFake 28 | implements _i2.AuthRepository { 29 | _FakeAuthRepository_0( 30 | Object parent, 31 | Invocation parentInvocation, 32 | ) : super( 33 | parent, 34 | parentInvocation, 35 | ); 36 | } 37 | 38 | class _FakeAuthUser_1 extends _i1.SmartFake implements _i3.AuthUser { 39 | _FakeAuthUser_1( 40 | Object parent, 41 | Invocation parentInvocation, 42 | ) : super( 43 | parent, 44 | parentInvocation, 45 | ); 46 | } 47 | 48 | /// A class which mocks [SignUpUseCase]. 49 | /// 50 | /// See the documentation for Mockito's code generation for more information. 51 | class MockSignUpUseCase extends _i1.Mock implements _i4.SignUpUseCase { 52 | MockSignUpUseCase() { 53 | _i1.throwOnMissingStub(this); 54 | } 55 | 56 | @override 57 | _i2.AuthRepository get authRepository => (super.noSuchMethod( 58 | Invocation.getter(#authRepository), 59 | returnValue: _FakeAuthRepository_0( 60 | this, 61 | Invocation.getter(#authRepository), 62 | ), 63 | ) as _i2.AuthRepository); 64 | @override 65 | _i5.Future<_i3.AuthUser> call(_i4.SignUpParams? params) => 66 | (super.noSuchMethod( 67 | Invocation.method( 68 | #call, 69 | [params], 70 | ), 71 | returnValue: _i5.Future<_i3.AuthUser>.value(_FakeAuthUser_1( 72 | this, 73 | Invocation.method( 74 | #call, 75 | [params], 76 | ), 77 | )), 78 | ) as _i5.Future<_i3.AuthUser>); 79 | } 80 | -------------------------------------------------------------------------------- /test/src/features/auth/presentation/blocs/sign_up/sign_up_state_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/value_objects/email.dart'; 2 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/value_objects/password.dart'; 3 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/email_status.dart'; 4 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/form_status.dart'; 5 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/password_status.dart'; 6 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/sign_up/sign_up_cubit.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | 9 | void main() { 10 | group('SignUpState', () { 11 | test('should correctly copy state with new email', () { 12 | const initialState = SignUpState(); 13 | final email = Email((email) => email..value = 'test@test.com'); 14 | 15 | final newState = initialState.copyWith( 16 | email: email, 17 | emailStatus: EmailStatus.valid, 18 | ); 19 | 20 | expect(newState.email, equals(email)); 21 | expect(newState.emailStatus, equals(EmailStatus.valid)); 22 | expect(newState.password, equals(initialState.password)); 23 | expect(newState.passwordStatus, equals(PasswordStatus.unknown)); 24 | }); 25 | 26 | test('should not update the email if the new value is invalid', () { 27 | const initialState = SignUpState(); 28 | 29 | try { 30 | final email = Email((email) => email..value = 'testtest'); 31 | // ignore: unused_local_variable 32 | final newState = initialState.copyWith(email: email); 33 | 34 | // If the above line did not throw an error, fail the test 35 | fail('Should have thrown an ArgumentError'); 36 | } on ArgumentError { 37 | // If an ArgumentError was thrown, continue the test 38 | expect(initialState.email, isNull); 39 | } 40 | }); 41 | 42 | test('should correctly copy state with new password', () { 43 | const initialState = SignUpState(); 44 | final password = Password((password) => password..value = 'password1234'); 45 | 46 | final newState = initialState.copyWith( 47 | password: password, 48 | passwordStatus: PasswordStatus.valid, 49 | ); 50 | 51 | expect(newState.password, equals(password)); 52 | expect(newState.passwordStatus, equals(PasswordStatus.valid)); 53 | expect(newState.email, equals(initialState.email)); 54 | expect(newState.emailStatus, equals(EmailStatus.unknown)); 55 | }); 56 | 57 | test('should not update the password if the new value is invalid', () { 58 | const initialState = SignUpState(); 59 | 60 | try { 61 | final password = Password((password) => password..value = 'test'); 62 | // ignore: unused_local_variable 63 | final newState = initialState.copyWith(password: password); 64 | 65 | // If the above line did not throw an error, fail the test 66 | fail('Should have thrown an ArgumentError'); 67 | } on ArgumentError { 68 | // If an ArgumentError was thrown, continue the test 69 | expect(initialState.password, isNull); 70 | } 71 | }); 72 | 73 | test('should correctly copy state with new formStatus', () { 74 | const initialState = SignUpState(); 75 | final newState = initialState.copyWith( 76 | formStatus: FormStatus.submissionInProgress, 77 | ); 78 | 79 | expect( 80 | newState.formStatus, 81 | equals(FormStatus.submissionInProgress), 82 | ); 83 | 84 | // The other fields do not change 85 | expect(newState.email, equals(initialState.email)); 86 | expect(newState.password, equals(initialState.password)); 87 | expect(newState.emailStatus, equals(initialState.emailStatus)); 88 | expect(newState.passwordStatus, equals(initialState.passwordStatus)); 89 | }); 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /test/src/features/auth/presentation/screens/sign_in_screen_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/repositories/auth_repository.dart'; 2 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/email_status.dart'; 3 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/form_status.dart'; 4 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/password_status.dart'; 5 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/sign_in/sign_in_cubit.dart'; 6 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/screens/sign_in_screen.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | import 'package:mockito/annotations.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter_bloc/flutter_bloc.dart'; 11 | import 'package:mockito/mockito.dart'; 12 | 13 | import 'sign_in_screen_test.mocks.dart'; 14 | 15 | @GenerateMocks([SignInCubit, AuthRepository]) 16 | void main() { 17 | const emailInputKey = Key('signIn_emailInput_textField'); 18 | const passwordInputKey = Key('signIn_passwordInput_textField'); 19 | const signInButtonKey = Key('signIn_continue_elevatedButton'); 20 | 21 | const tEmail = 'test@gmail.com'; 22 | const tPassword = 'password12345'; 23 | 24 | late MockSignInCubit mockSignInCubit; 25 | late MockAuthRepository mockAuthRepository; 26 | 27 | Widget makeTestableWidget() { 28 | return MaterialApp( 29 | home: BlocProvider( 30 | create: (_) => mockSignInCubit, 31 | child: const SignInView(), 32 | ), 33 | ); 34 | } 35 | 36 | setUp(() { 37 | mockSignInCubit = MockSignInCubit(); 38 | mockAuthRepository = MockAuthRepository(); 39 | when(mockSignInCubit.state).thenReturn(const SignInState()); 40 | // Stub the state stream. 41 | when(mockSignInCubit.stream).thenAnswer( 42 | (_) => Stream.fromIterable([const SignInState()]), 43 | ); 44 | }); 45 | 46 | testWidgets('renders a SignInScreen', (tester) async { 47 | await tester.pumpWidget( 48 | RepositoryProvider( 49 | create: (_) => mockAuthRepository, 50 | child: const MaterialApp(home: SignInScreen()), 51 | ), 52 | ); 53 | 54 | expect(find.byType(SignInView), findsOneWidget); 55 | }); 56 | 57 | testWidgets('emailChanged when email changes with 500 milliseconds debounce', 58 | (tester) async { 59 | await tester.pumpWidget(makeTestableWidget()); 60 | await tester.enterText(find.byKey(emailInputKey), tEmail); 61 | await tester.pump(const Duration(milliseconds: 500)); 62 | verify(mockSignInCubit.emailChanged(tEmail)).called(1); 63 | }); 64 | 65 | testWidgets( 66 | 'passwordChanged when email changes with 500 milliseconds debounce', 67 | (tester) async { 68 | await tester.pumpWidget(makeTestableWidget()); 69 | 70 | await tester.enterText(find.byKey(passwordInputKey), tPassword); 71 | await tester.pump(const Duration(milliseconds: 500)); 72 | verify(mockSignInCubit.passwordChanged(tPassword)).called(1); 73 | }); 74 | 75 | testWidgets('AppBar & ElevatedButton are present with correct text', 76 | (tester) async { 77 | await tester.pumpWidget(makeTestableWidget()); 78 | expect(find.byType(AppBar), findsOneWidget); 79 | expect(find.byType(ElevatedButton), findsOneWidget); 80 | expect(find.text('Sign In'), findsWidgets); 81 | }); 82 | 83 | testWidgets('Two TextFormFields are present', (tester) async { 84 | await tester.pumpWidget(makeTestableWidget()); 85 | 86 | expect(find.byType(TextFormField), findsNWidgets(2)); 87 | }); 88 | testWidgets('Show snack bar when the form status is invalid', 89 | (WidgetTester tester) async { 90 | //Arrange 91 | const text = 'Invalid form: please fill in all fields'; 92 | final expectedStates = [ 93 | const SignInState(formStatus: FormStatus.initial), 94 | const SignInState(formStatus: FormStatus.invalid), 95 | ]; 96 | 97 | when(mockSignInCubit.stream).thenAnswer( 98 | (_) => Stream.fromIterable(expectedStates), 99 | ); 100 | 101 | //Act 102 | await tester.pumpWidget(makeTestableWidget()); 103 | expect(find.text(text), findsNothing); 104 | await tester.pump(); 105 | 106 | //Assert 107 | expect(find.text(text), findsOneWidget); 108 | }); 109 | 110 | testWidgets('Show snack bar when the form status is submissionFailure', 111 | (WidgetTester tester) async { 112 | //Arrange 113 | const text = 'There was an error with the sign in process. Try again.'; 114 | final expectedStates = [ 115 | const SignInState(formStatus: FormStatus.initial), 116 | const SignInState(formStatus: FormStatus.submissionFailure), 117 | ]; 118 | 119 | when(mockSignInCubit.stream).thenAnswer( 120 | (_) => Stream.fromIterable(expectedStates), 121 | ); 122 | 123 | //Act 124 | await tester.pumpWidget(makeTestableWidget()); 125 | expect(find.text(text), findsNothing); 126 | await tester.pump(); 127 | 128 | //Assert 129 | expect(find.text(text), findsOneWidget); 130 | }); 131 | 132 | testWidgets('Email field shows error for invalid email', (tester) async { 133 | when(mockSignInCubit.state).thenReturn( 134 | const SignInState( 135 | emailStatus: EmailStatus.invalid, 136 | ), 137 | ); 138 | await tester.pumpWidget(makeTestableWidget()); 139 | 140 | expect(find.text('Invalid email'), findsOneWidget); 141 | }); 142 | 143 | testWidgets('Password field shows error for invalid password', 144 | (tester) async { 145 | when(mockSignInCubit.state) 146 | .thenReturn(const SignInState(passwordStatus: PasswordStatus.invalid)); 147 | await tester.pumpWidget(makeTestableWidget()); 148 | 149 | expect(find.text('Invalid password'), findsOneWidget); 150 | }); 151 | 152 | testWidgets('SignIn function is called when button is pressed', 153 | (tester) async { 154 | await tester.pumpWidget(makeTestableWidget()); 155 | await tester.tap(find.byType(ElevatedButton)); 156 | verify(mockSignInCubit.signIn()).called(1); 157 | }); 158 | 159 | testWidgets( 160 | 'SignIn button is disabled when formStatus is submissionInProgress', 161 | (tester) async { 162 | when(mockSignInCubit.state).thenReturn( 163 | const SignInState( 164 | formStatus: FormStatus.submissionInProgress, 165 | ), 166 | ); 167 | await tester.pumpWidget(makeTestableWidget()); 168 | expect( 169 | // tester.widget(find.byType(ElevatedButton)).enabled, 170 | tester.widget(find.byKey(signInButtonKey)).enabled, 171 | isFalse, 172 | ); 173 | }); 174 | } 175 | -------------------------------------------------------------------------------- /test/src/features/auth/presentation/screens/sign_in_screen_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.4.0 from annotations 2 | // in flutter_clean_architecture_with_firebase/test/src/features/auth/presentation/screens/sign_in_screen_test.dart. 3 | // Do not manually edit this file. 4 | 5 | // ignore_for_file: no_leading_underscores_for_library_prefixes 6 | import 'dart:async' as _i4; 7 | 8 | import 'package:bloc/bloc.dart' as _i5; 9 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/entities/auth_user.dart' 10 | as _i3; 11 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/repositories/auth_repository.dart' 12 | as _i6; 13 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/sign_in/sign_in_cubit.dart' 14 | as _i2; 15 | import 'package:mockito/mockito.dart' as _i1; 16 | 17 | // ignore_for_file: type=lint 18 | // ignore_for_file: avoid_redundant_argument_values 19 | // ignore_for_file: avoid_setters_without_getters 20 | // ignore_for_file: comment_references 21 | // ignore_for_file: implementation_imports 22 | // ignore_for_file: invalid_use_of_visible_for_testing_member 23 | // ignore_for_file: prefer_const_constructors 24 | // ignore_for_file: unnecessary_parenthesis 25 | // ignore_for_file: camel_case_types 26 | // ignore_for_file: subtype_of_sealed_class 27 | 28 | class _FakeSignInState_0 extends _i1.SmartFake implements _i2.SignInState { 29 | _FakeSignInState_0( 30 | Object parent, 31 | Invocation parentInvocation, 32 | ) : super( 33 | parent, 34 | parentInvocation, 35 | ); 36 | } 37 | 38 | class _FakeAuthUser_1 extends _i1.SmartFake implements _i3.AuthUser { 39 | _FakeAuthUser_1( 40 | Object parent, 41 | Invocation parentInvocation, 42 | ) : super( 43 | parent, 44 | parentInvocation, 45 | ); 46 | } 47 | 48 | /// A class which mocks [SignInCubit]. 49 | /// 50 | /// See the documentation for Mockito's code generation for more information. 51 | class MockSignInCubit extends _i1.Mock implements _i2.SignInCubit { 52 | MockSignInCubit() { 53 | _i1.throwOnMissingStub(this); 54 | } 55 | 56 | @override 57 | _i2.SignInState get state => (super.noSuchMethod( 58 | Invocation.getter(#state), 59 | returnValue: _FakeSignInState_0( 60 | this, 61 | Invocation.getter(#state), 62 | ), 63 | ) as _i2.SignInState); 64 | @override 65 | _i4.Stream<_i2.SignInState> get stream => (super.noSuchMethod( 66 | Invocation.getter(#stream), 67 | returnValue: _i4.Stream<_i2.SignInState>.empty(), 68 | ) as _i4.Stream<_i2.SignInState>); 69 | @override 70 | bool get isClosed => (super.noSuchMethod( 71 | Invocation.getter(#isClosed), 72 | returnValue: false, 73 | ) as bool); 74 | @override 75 | void emailChanged(String? value) => super.noSuchMethod( 76 | Invocation.method( 77 | #emailChanged, 78 | [value], 79 | ), 80 | returnValueForMissingStub: null, 81 | ); 82 | @override 83 | void passwordChanged(String? value) => super.noSuchMethod( 84 | Invocation.method( 85 | #passwordChanged, 86 | [value], 87 | ), 88 | returnValueForMissingStub: null, 89 | ); 90 | @override 91 | _i4.Future signIn() => (super.noSuchMethod( 92 | Invocation.method( 93 | #signIn, 94 | [], 95 | ), 96 | returnValue: _i4.Future.value(), 97 | returnValueForMissingStub: _i4.Future.value(), 98 | ) as _i4.Future); 99 | @override 100 | void emit(_i2.SignInState? state) => super.noSuchMethod( 101 | Invocation.method( 102 | #emit, 103 | [state], 104 | ), 105 | returnValueForMissingStub: null, 106 | ); 107 | @override 108 | void onChange(_i5.Change<_i2.SignInState>? change) => super.noSuchMethod( 109 | Invocation.method( 110 | #onChange, 111 | [change], 112 | ), 113 | returnValueForMissingStub: null, 114 | ); 115 | @override 116 | void addError( 117 | Object? error, [ 118 | StackTrace? stackTrace, 119 | ]) => 120 | super.noSuchMethod( 121 | Invocation.method( 122 | #addError, 123 | [ 124 | error, 125 | stackTrace, 126 | ], 127 | ), 128 | returnValueForMissingStub: null, 129 | ); 130 | @override 131 | void onError( 132 | Object? error, 133 | StackTrace? stackTrace, 134 | ) => 135 | super.noSuchMethod( 136 | Invocation.method( 137 | #onError, 138 | [ 139 | error, 140 | stackTrace, 141 | ], 142 | ), 143 | returnValueForMissingStub: null, 144 | ); 145 | @override 146 | _i4.Future close() => (super.noSuchMethod( 147 | Invocation.method( 148 | #close, 149 | [], 150 | ), 151 | returnValue: _i4.Future.value(), 152 | returnValueForMissingStub: _i4.Future.value(), 153 | ) as _i4.Future); 154 | } 155 | 156 | /// A class which mocks [AuthRepository]. 157 | /// 158 | /// See the documentation for Mockito's code generation for more information. 159 | class MockAuthRepository extends _i1.Mock implements _i6.AuthRepository { 160 | MockAuthRepository() { 161 | _i1.throwOnMissingStub(this); 162 | } 163 | 164 | @override 165 | _i4.Stream<_i3.AuthUser> get authUser => (super.noSuchMethod( 166 | Invocation.getter(#authUser), 167 | returnValue: _i4.Stream<_i3.AuthUser>.empty(), 168 | ) as _i4.Stream<_i3.AuthUser>); 169 | @override 170 | _i4.Future<_i3.AuthUser> signUp({ 171 | required String? email, 172 | required String? password, 173 | }) => 174 | (super.noSuchMethod( 175 | Invocation.method( 176 | #signUp, 177 | [], 178 | { 179 | #email: email, 180 | #password: password, 181 | }, 182 | ), 183 | returnValue: _i4.Future<_i3.AuthUser>.value(_FakeAuthUser_1( 184 | this, 185 | Invocation.method( 186 | #signUp, 187 | [], 188 | { 189 | #email: email, 190 | #password: password, 191 | }, 192 | ), 193 | )), 194 | ) as _i4.Future<_i3.AuthUser>); 195 | @override 196 | _i4.Future<_i3.AuthUser> signIn({ 197 | required String? email, 198 | required String? password, 199 | }) => 200 | (super.noSuchMethod( 201 | Invocation.method( 202 | #signIn, 203 | [], 204 | { 205 | #email: email, 206 | #password: password, 207 | }, 208 | ), 209 | returnValue: _i4.Future<_i3.AuthUser>.value(_FakeAuthUser_1( 210 | this, 211 | Invocation.method( 212 | #signIn, 213 | [], 214 | { 215 | #email: email, 216 | #password: password, 217 | }, 218 | ), 219 | )), 220 | ) as _i4.Future<_i3.AuthUser>); 221 | @override 222 | _i4.Future signOut() => (super.noSuchMethod( 223 | Invocation.method( 224 | #signOut, 225 | [], 226 | ), 227 | returnValue: _i4.Future.value(), 228 | returnValueForMissingStub: _i4.Future.value(), 229 | ) as _i4.Future); 230 | } 231 | -------------------------------------------------------------------------------- /test/src/features/auth/presentation/screens/sign_up_screen_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/repositories/auth_repository.dart'; 4 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/email_status.dart'; 5 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/form_status.dart'; 6 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/password_status.dart'; 7 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/sign_up/sign_up_cubit.dart'; 8 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/screens/sign_up_screen.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | import 'package:mockito/annotations.dart'; 11 | import 'package:mockito/mockito.dart'; 12 | 13 | import 'sign_up_screen_test.mocks.dart'; 14 | 15 | @GenerateMocks([SignUpCubit, AuthRepository]) 16 | void main() { 17 | const emailInputKey = Key('signUp_emailInput_textField'); 18 | const passwordInputKey = Key('signUp_passwordInput_textField'); 19 | const signUpButtonKey = Key('signUp_continue_elevatedButton'); 20 | 21 | const tEmail = 'test@gmail.com'; 22 | const tPassword = 'password12345'; 23 | 24 | late MockSignUpCubit mockSignUpCubit; 25 | late MockAuthRepository mockAuthRepository; 26 | 27 | Widget makeTestableWidget() { 28 | return MaterialApp( 29 | home: BlocProvider( 30 | create: (_) => mockSignUpCubit, 31 | child: const SignUpView(), 32 | ), 33 | ); 34 | } 35 | 36 | setUp(() { 37 | mockSignUpCubit = MockSignUpCubit(); 38 | mockAuthRepository = MockAuthRepository(); 39 | when(mockSignUpCubit.state).thenReturn(const SignUpState()); 40 | // Stub the state stream. 41 | when(mockSignUpCubit.stream).thenAnswer( 42 | (_) => Stream.fromIterable([const SignUpState()]), 43 | ); 44 | }); 45 | 46 | testWidgets('renders a SignUpScreen', (tester) async { 47 | await tester.pumpWidget( 48 | RepositoryProvider( 49 | create: (_) => mockAuthRepository, 50 | child: const MaterialApp(home: SignUpScreen()), 51 | ), 52 | ); 53 | 54 | expect(find.byType(SignUpView), findsOneWidget); 55 | }); 56 | 57 | testWidgets('emailChanged when email changes with 500 milliseconds debounce', 58 | (tester) async { 59 | await tester.pumpWidget(makeTestableWidget()); 60 | await tester.enterText(find.byKey(emailInputKey), tEmail); 61 | await tester.pump(const Duration(milliseconds: 500)); 62 | verify(mockSignUpCubit.emailChanged(tEmail)).called(1); 63 | }); 64 | 65 | testWidgets( 66 | 'passwordChanged when email changes with 500 milliseconds debounce', 67 | (tester) async { 68 | await tester.pumpWidget(makeTestableWidget()); 69 | 70 | await tester.enterText(find.byKey(passwordInputKey), tPassword); 71 | await tester.pump(const Duration(milliseconds: 500)); 72 | verify(mockSignUpCubit.passwordChanged(tPassword)).called(1); 73 | }); 74 | 75 | testWidgets('AppBar & ElevatedButton are present with correct text', 76 | (tester) async { 77 | await tester.pumpWidget(makeTestableWidget()); 78 | expect(find.byType(AppBar), findsOneWidget); 79 | expect(find.byType(ElevatedButton), findsOneWidget); 80 | expect(find.text('Sign Up'), findsWidgets); 81 | }); 82 | 83 | testWidgets('Two TextFormFields are present', (tester) async { 84 | await tester.pumpWidget(makeTestableWidget()); 85 | 86 | expect(find.byType(TextFormField), findsNWidgets(2)); 87 | }); 88 | testWidgets('Show snack bar when the form status is invalid', 89 | (WidgetTester tester) async { 90 | //Arrange 91 | const text = 'Invalid form: please fill in all fields'; 92 | final expectedStates = [ 93 | const SignUpState(formStatus: FormStatus.initial), 94 | const SignUpState(formStatus: FormStatus.invalid), 95 | ]; 96 | 97 | when(mockSignUpCubit.stream).thenAnswer( 98 | (_) => Stream.fromIterable(expectedStates), 99 | ); 100 | 101 | //Act 102 | await tester.pumpWidget(makeTestableWidget()); 103 | expect(find.text(text), findsNothing); 104 | await tester.pump(); 105 | 106 | //Assert 107 | expect(find.text(text), findsOneWidget); 108 | }); 109 | 110 | testWidgets('Show snack bar when the form status is submissionFailure', 111 | (WidgetTester tester) async { 112 | //Arrange 113 | const text = 'There was an error with the sign up process. Try again.'; 114 | final expectedStates = [ 115 | const SignUpState(formStatus: FormStatus.initial), 116 | const SignUpState(formStatus: FormStatus.submissionFailure), 117 | ]; 118 | 119 | when(mockSignUpCubit.stream).thenAnswer( 120 | (_) => Stream.fromIterable(expectedStates), 121 | ); 122 | 123 | //Act 124 | await tester.pumpWidget(makeTestableWidget()); 125 | expect(find.text(text), findsNothing); 126 | await tester.pump(); 127 | 128 | //Assert 129 | expect(find.text(text), findsOneWidget); 130 | }); 131 | 132 | testWidgets('Email field shows error for invalid email', (tester) async { 133 | when(mockSignUpCubit.state).thenReturn( 134 | const SignUpState( 135 | emailStatus: EmailStatus.invalid, 136 | ), 137 | ); 138 | await tester.pumpWidget(makeTestableWidget()); 139 | 140 | expect(find.text('Invalid email'), findsOneWidget); 141 | }); 142 | 143 | testWidgets('Password field shows error for invalid password', 144 | (tester) async { 145 | when(mockSignUpCubit.state) 146 | .thenReturn(const SignUpState(passwordStatus: PasswordStatus.invalid)); 147 | await tester.pumpWidget(makeTestableWidget()); 148 | 149 | expect(find.text('Invalid password'), findsOneWidget); 150 | }); 151 | 152 | testWidgets('SignUp function is called when button is pressed', 153 | (tester) async { 154 | await tester.pumpWidget(makeTestableWidget()); 155 | await tester.tap(find.byType(ElevatedButton)); 156 | verify(mockSignUpCubit.signUp()).called(1); 157 | }); 158 | 159 | testWidgets( 160 | 'SignUp button is disabled when formStatus is submissionInProgress', 161 | (tester) async { 162 | when(mockSignUpCubit.state).thenReturn( 163 | const SignUpState( 164 | formStatus: FormStatus.submissionInProgress, 165 | ), 166 | ); 167 | await tester.pumpWidget(makeTestableWidget()); 168 | expect( 169 | // tester.widget(find.byType(ElevatedButton)).enabled, 170 | tester.widget(find.byKey(signUpButtonKey)).enabled, 171 | isFalse, 172 | ); 173 | }); 174 | } 175 | -------------------------------------------------------------------------------- /test/src/features/auth/presentation/screens/sign_up_screen_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.4.0 from annotations 2 | // in flutter_clean_architecture_with_firebase/test/src/features/auth/presentation/screens/sign_up_screen_test.dart. 3 | // Do not manually edit this file. 4 | 5 | // ignore_for_file: no_leading_underscores_for_library_prefixes 6 | import 'dart:async' as _i4; 7 | 8 | import 'package:bloc/bloc.dart' as _i5; 9 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/entities/auth_user.dart' 10 | as _i3; 11 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/domain/repositories/auth_repository.dart' 12 | as _i6; 13 | import 'package:flutter_clean_architecture_with_firebase/src/features/auth/presentation/blocs/sign_up/sign_up_cubit.dart' 14 | as _i2; 15 | import 'package:mockito/mockito.dart' as _i1; 16 | 17 | // ignore_for_file: type=lint 18 | // ignore_for_file: avoid_redundant_argument_values 19 | // ignore_for_file: avoid_setters_without_getters 20 | // ignore_for_file: comment_references 21 | // ignore_for_file: implementation_imports 22 | // ignore_for_file: invalid_use_of_visible_for_testing_member 23 | // ignore_for_file: prefer_const_constructors 24 | // ignore_for_file: unnecessary_parenthesis 25 | // ignore_for_file: camel_case_types 26 | // ignore_for_file: subtype_of_sealed_class 27 | 28 | class _FakeSignUpState_0 extends _i1.SmartFake implements _i2.SignUpState { 29 | _FakeSignUpState_0( 30 | Object parent, 31 | Invocation parentInvocation, 32 | ) : super( 33 | parent, 34 | parentInvocation, 35 | ); 36 | } 37 | 38 | class _FakeAuthUser_1 extends _i1.SmartFake implements _i3.AuthUser { 39 | _FakeAuthUser_1( 40 | Object parent, 41 | Invocation parentInvocation, 42 | ) : super( 43 | parent, 44 | parentInvocation, 45 | ); 46 | } 47 | 48 | /// A class which mocks [SignUpCubit]. 49 | /// 50 | /// See the documentation for Mockito's code generation for more information. 51 | class MockSignUpCubit extends _i1.Mock implements _i2.SignUpCubit { 52 | MockSignUpCubit() { 53 | _i1.throwOnMissingStub(this); 54 | } 55 | 56 | @override 57 | _i2.SignUpState get state => (super.noSuchMethod( 58 | Invocation.getter(#state), 59 | returnValue: _FakeSignUpState_0( 60 | this, 61 | Invocation.getter(#state), 62 | ), 63 | ) as _i2.SignUpState); 64 | @override 65 | _i4.Stream<_i2.SignUpState> get stream => (super.noSuchMethod( 66 | Invocation.getter(#stream), 67 | returnValue: _i4.Stream<_i2.SignUpState>.empty(), 68 | ) as _i4.Stream<_i2.SignUpState>); 69 | @override 70 | bool get isClosed => (super.noSuchMethod( 71 | Invocation.getter(#isClosed), 72 | returnValue: false, 73 | ) as bool); 74 | @override 75 | void emailChanged(String? value) => super.noSuchMethod( 76 | Invocation.method( 77 | #emailChanged, 78 | [value], 79 | ), 80 | returnValueForMissingStub: null, 81 | ); 82 | @override 83 | void passwordChanged(String? value) => super.noSuchMethod( 84 | Invocation.method( 85 | #passwordChanged, 86 | [value], 87 | ), 88 | returnValueForMissingStub: null, 89 | ); 90 | @override 91 | _i4.Future signUp() => (super.noSuchMethod( 92 | Invocation.method( 93 | #signUp, 94 | [], 95 | ), 96 | returnValue: _i4.Future.value(), 97 | returnValueForMissingStub: _i4.Future.value(), 98 | ) as _i4.Future); 99 | @override 100 | void emit(_i2.SignUpState? state) => super.noSuchMethod( 101 | Invocation.method( 102 | #emit, 103 | [state], 104 | ), 105 | returnValueForMissingStub: null, 106 | ); 107 | @override 108 | void onChange(_i5.Change<_i2.SignUpState>? change) => super.noSuchMethod( 109 | Invocation.method( 110 | #onChange, 111 | [change], 112 | ), 113 | returnValueForMissingStub: null, 114 | ); 115 | @override 116 | void addError( 117 | Object? error, [ 118 | StackTrace? stackTrace, 119 | ]) => 120 | super.noSuchMethod( 121 | Invocation.method( 122 | #addError, 123 | [ 124 | error, 125 | stackTrace, 126 | ], 127 | ), 128 | returnValueForMissingStub: null, 129 | ); 130 | @override 131 | void onError( 132 | Object? error, 133 | StackTrace? stackTrace, 134 | ) => 135 | super.noSuchMethod( 136 | Invocation.method( 137 | #onError, 138 | [ 139 | error, 140 | stackTrace, 141 | ], 142 | ), 143 | returnValueForMissingStub: null, 144 | ); 145 | @override 146 | _i4.Future close() => (super.noSuchMethod( 147 | Invocation.method( 148 | #close, 149 | [], 150 | ), 151 | returnValue: _i4.Future.value(), 152 | returnValueForMissingStub: _i4.Future.value(), 153 | ) as _i4.Future); 154 | } 155 | 156 | /// A class which mocks [AuthRepository]. 157 | /// 158 | /// See the documentation for Mockito's code generation for more information. 159 | class MockAuthRepository extends _i1.Mock implements _i6.AuthRepository { 160 | MockAuthRepository() { 161 | _i1.throwOnMissingStub(this); 162 | } 163 | 164 | @override 165 | _i4.Stream<_i3.AuthUser> get authUser => (super.noSuchMethod( 166 | Invocation.getter(#authUser), 167 | returnValue: _i4.Stream<_i3.AuthUser>.empty(), 168 | ) as _i4.Stream<_i3.AuthUser>); 169 | @override 170 | _i4.Future<_i3.AuthUser> signUp({ 171 | required String? email, 172 | required String? password, 173 | }) => 174 | (super.noSuchMethod( 175 | Invocation.method( 176 | #signUp, 177 | [], 178 | { 179 | #email: email, 180 | #password: password, 181 | }, 182 | ), 183 | returnValue: _i4.Future<_i3.AuthUser>.value(_FakeAuthUser_1( 184 | this, 185 | Invocation.method( 186 | #signUp, 187 | [], 188 | { 189 | #email: email, 190 | #password: password, 191 | }, 192 | ), 193 | )), 194 | ) as _i4.Future<_i3.AuthUser>); 195 | @override 196 | _i4.Future<_i3.AuthUser> signIn({ 197 | required String? email, 198 | required String? password, 199 | }) => 200 | (super.noSuchMethod( 201 | Invocation.method( 202 | #signIn, 203 | [], 204 | { 205 | #email: email, 206 | #password: password, 207 | }, 208 | ), 209 | returnValue: _i4.Future<_i3.AuthUser>.value(_FakeAuthUser_1( 210 | this, 211 | Invocation.method( 212 | #signIn, 213 | [], 214 | { 215 | #email: email, 216 | #password: password, 217 | }, 218 | ), 219 | )), 220 | ) as _i4.Future<_i3.AuthUser>); 221 | @override 222 | _i4.Future signOut() => (super.noSuchMethod( 223 | Invocation.method( 224 | #signOut, 225 | [], 226 | ), 227 | returnValue: _i4.Future.value(), 228 | returnValueForMissingStub: _i4.Future.value(), 229 | ) as _i4.Future); 230 | } 231 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxonflutter/Flutter-Clean-Architecture-With-Firebase/b49ed2200d1332aa3c473f5de943d0db2191af19/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | flutter_clean_architecture_with_firebase 34 | 35 | 36 | 40 | 41 | 42 | 43 | 44 | 45 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter_clean_architecture_with_firebase", 3 | "short_name": "flutter_clean_architecture_with_firebase", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } --------------------------------------------------------------------------------