├── .firebaserc ├── .github └── workflows │ ├── store_deploy.yml │ └── tests.yml ├── .gitignore ├── .metadata ├── .vscode ├── README.md ├── extensions.json └── settings.json ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ ├── google-services.json │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── dev │ │ │ └── kaio │ │ │ └── financy │ │ │ └── financy_app │ │ │ └── MainActivityTest.java │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── dev │ │ │ │ └── kaio │ │ │ │ └── financy │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── fonts │ ├── Inter-Bold.ttf │ ├── Inter-ExtraBold.ttf │ ├── Inter-Medium.ttf │ ├── Inter-Regular.ttf │ └── Inter-SemiBold.ttf └── images │ ├── check_your_email_image.png │ ├── forgot_password_image.png │ ├── man.png │ ├── onboarding_image.png │ ├── sign_in_image.png │ └── sign_up_image.png ├── coverage.sh ├── firebase.json ├── functions ├── .gitignore ├── index.js ├── package-lock.json └── package.json ├── integration_test ├── integration_test.dart ├── robots │ ├── forgot_password_robot.dart │ ├── onboarding_robot.dart │ ├── robot_extension.dart │ ├── sign_in_robot.dart │ └── sign_out_robot.dart └── utils.dart ├── 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 │ │ │ ├── AppIcon-20@2x.png │ │ │ ├── AppIcon-20@2x~ipad.png │ │ │ ├── AppIcon-20@3x.png │ │ │ ├── AppIcon-20~ipad.png │ │ │ ├── AppIcon-29.png │ │ │ ├── AppIcon-29@2x.png │ │ │ ├── AppIcon-29@2x~ipad.png │ │ │ ├── AppIcon-29@3x.png │ │ │ ├── AppIcon-29~ipad.png │ │ │ ├── AppIcon-40@2x.png │ │ │ ├── AppIcon-40@2x~ipad.png │ │ │ ├── AppIcon-40@3x.png │ │ │ ├── AppIcon-40~ipad.png │ │ │ ├── AppIcon-60@2x~car.png │ │ │ ├── AppIcon-60@3x~car.png │ │ │ ├── AppIcon-83.5@2x~ipad.png │ │ │ ├── AppIcon@2x.png │ │ │ ├── AppIcon@2x~ipad.png │ │ │ ├── AppIcon@3x.png │ │ │ ├── AppIcon~ios-marketing.png │ │ │ ├── AppIcon~ipad.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── ExportOptions.plist │ ├── GoogleService-Info.plist │ ├── Info.plist │ └── Runner-Bridging-Header.h └── firebase_app_id_file.json ├── lib ├── app.dart ├── common │ ├── constants │ │ ├── app_colors.dart │ │ ├── app_text_styles.dart │ │ ├── constants.dart │ │ ├── date.dart │ │ ├── environment.dart │ │ ├── keys.dart │ │ ├── mutations.dart │ │ ├── queries.dart │ │ └── routes.dart │ ├── data │ │ ├── data.dart │ │ ├── data_result.dart │ │ └── exceptions.dart │ ├── extensions │ │ ├── date_formatter.dart │ │ ├── extensions.dart │ │ ├── page_controller_ext.dart │ │ ├── sizes.dart │ │ └── types_ext.dart │ ├── features │ │ ├── balance │ │ │ ├── balance.dart │ │ │ ├── balance_controller.dart │ │ │ └── balance_state.dart │ │ └── transaction │ │ │ ├── transaction.dart │ │ │ ├── transaction_controller.dart │ │ │ └── transaction_state.dart │ ├── models │ │ ├── balances_model.dart │ │ ├── models.dart │ │ ├── transaction_model.dart │ │ └── user_model.dart │ ├── themes │ │ └── default_theme.dart │ ├── utils │ │ ├── money_mask_controller.dart │ │ ├── uppercase_text_formatter.dart │ │ ├── utils.dart │ │ └── validator.dart │ └── widgets │ │ ├── app_header.dart │ │ ├── base_page.dart │ │ ├── custom_bottom_app_bar.dart │ │ ├── custom_bottom_sheet.dart │ │ ├── custom_circular_progress_indicator.dart │ │ ├── custom_snackbar.dart │ │ ├── custom_text_form_field.dart │ │ ├── greetings_widget.dart │ │ ├── multi_text_button.dart │ │ ├── notification_widget.dart │ │ ├── password_form_field.dart │ │ ├── primary_button.dart │ │ ├── transaction_listview.dart │ │ └── widgets.dart ├── features │ ├── forgot_password │ │ ├── check_your_email_page.dart │ │ ├── forgot_password_controller.dart │ │ ├── forgot_password_page.dart │ │ └── forgot_password_state.dart │ ├── home │ │ ├── home.dart │ │ ├── home_controller.dart │ │ ├── home_page.dart │ │ ├── home_page_view.dart │ │ ├── home_state.dart │ │ └── widgets │ │ │ └── balance_card_widget.dart │ ├── onboarding │ │ ├── onboarding.dart │ │ └── onboarding_page.dart │ ├── profile │ │ ├── profile.dart │ │ ├── profile_controller.dart │ │ ├── profile_page.dart │ │ ├── profile_state.dart │ │ └── widgets │ │ │ ├── profile_change_name_widget.dart │ │ │ └── profile_change_password_widget.dart │ ├── sign_in │ │ ├── sign_in.dart │ │ ├── sign_in_controller.dart │ │ ├── sign_in_page.dart │ │ └── sign_in_state.dart │ ├── sign_up │ │ ├── sign_up.dart │ │ ├── sign_up_controller.dart │ │ ├── sign_up_page.dart │ │ └── sign_up_state.dart │ ├── splash │ │ ├── splash.dart │ │ ├── splash_controller.dart │ │ ├── splash_page.dart │ │ └── splash_state.dart │ ├── stats │ │ ├── stats.dart │ │ ├── stats_controller.dart │ │ ├── stats_page.dart │ │ └── stats_state.dart │ ├── transactions │ │ ├── transaction_page.dart │ │ └── transactions.dart │ └── wallet │ │ ├── wallet.dart │ │ ├── wallet_controller.dart │ │ ├── wallet_page.dart │ │ └── wallet_state.dart ├── firebase_options.dart ├── locator.dart ├── main.dart ├── repositories │ ├── repositories.dart │ ├── transaction_repository.dart │ └── transaction_repository_impl.dart └── services │ ├── auth_service │ ├── auth_service.dart │ └── firebase_auth_service.dart │ ├── connection_service.dart │ ├── data_service │ ├── data_service.dart │ ├── database_service.dart │ └── graphql_service.dart │ ├── secure_storage.dart │ ├── services.dart │ ├── sync_service │ ├── sync_controller.dart │ ├── sync_service.dart │ └── sync_state.dart │ └── user_data_service │ ├── user_data_service.dart │ └── user_data_service_impl.dart ├── pubspec.lock ├── pubspec.yaml └── test ├── common ├── features │ ├── balance │ │ └── balance_controller_test.dart │ └── transaction │ │ └── transaction_controller_test.dart └── widgets │ └── primary_button_test.dart ├── features ├── forgot_password │ └── forgot_password_controller_test.dart ├── home │ └── home_controller_test.dart ├── profile │ └── profile_controller_test.dart ├── sign_in │ └── sign_in_controller_test.dart ├── sign_up │ └── sign_up_controller_test.dart ├── splash │ └── splash_controller_test.dart ├── stats │ └── stats_controller_test.dart └── wallet │ └── wallet_controller_test.dart ├── mock └── mock_classes.dart ├── repositories └── transaction_repository_test.dart └── services ├── firebase_auth_service_test.dart ├── sync_service └── sync_controller_test.dart └── user_data_service └── user_data_service_test.dart /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "financy-app-6650d" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | 8 | env: 9 | GCLOUD_PROJECT_ID: ${{ secrets.GCLOUD_PROJECT_ID }} 10 | GCLOUD_SERVICE_KEY: ${{ secrets.GCLOUD_SERVICE_KEY }} 11 | JAVA_VERSION: 17 12 | FLUTTER_VERSION: '3.16.7' 13 | FLUTTER_CHANNEL: 'stable' 14 | 15 | jobs: 16 | unit_tests: 17 | name: Unit Tests 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v2 22 | 23 | - name: Set up Flutter ${{ env.FLUTTER_VERSION }} 24 | uses: subosito/flutter-action@v2 25 | with: 26 | flutter-version: ${{ env.FLUTTER_VERSION }} 27 | channel: ${{ env.FLUTTER_CHANNEL }} 28 | 29 | - name: Install dependencies 30 | run: flutter pub get 31 | 32 | - name: Generate test files 33 | run: | 34 | chmod +x ./coverage.sh 35 | ./coverage.sh 36 | 37 | - name: Unit Tests 38 | run: flutter test --coverage 39 | 40 | - name: Install lcov 41 | run: sudo apt-get install -y lcov 42 | 43 | - name: Remove generated files from code coverage report 44 | run: lcov --remove coverage/lcov.info 'lib/*/*.freezed.dart' 'lib/*/*.g.dart' 'lib/*/*.part.dart' 'lib/generated/*.dart' 'lib/generated/*/*.dart' o coverage/lcov.info 45 | 46 | - name: Upload code coverage to Codecov 47 | uses: codecov/codecov-action@v3 48 | with: 49 | token: ${{ secrets.CODECOV_TOKEN }} 50 | file: coverage/lcov.info 51 | flags: unittests 52 | 53 | integration_tests: 54 | runs-on: ubuntu-latest 55 | name: Integration Tests 56 | needs: unit_tests 57 | if: github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'master' && startsWith(github.event.pull_request.head.ref, 'release/') 58 | 59 | steps: 60 | - name: Checkout code 61 | uses: actions/checkout@v2 62 | 63 | - name: Set up Java ${{ env.JAVA_VERSION }} 64 | uses: actions/setup-java@v2 65 | with: 66 | java-version: ${{ env.JAVA_VERSION }} 67 | distribution: 'adopt' 68 | 69 | - name: Set up Flutter ${{ env.FLUTTER_VERSION }} 70 | uses: subosito/flutter-action@v2 71 | with: 72 | flutter-version: ${{ env.FLUTTER_VERSION }} 73 | channel: ${{ env.FLUTTER_CHANNEL }} 74 | 75 | - name: Install dependencies 76 | run: flutter pub get 77 | 78 | - name: Build Debug APK 79 | run: flutter build apk --debug --dart-define=GRAPHQL_ENDPOINT=https://api.financy.kaio.dev/v1/graphql 80 | 81 | - name: Build Instrumentation Test APK 82 | run: | 83 | pushd android 84 | ./gradlew app:assembleDebugAndroidTest 85 | ./gradlew app:assembleDebug -Ptarget="integration_test/integration_test.dart" 86 | popd 87 | 88 | # To Run Test Locally 89 | #./gradlew app:connectedDebugAndroidTest -Ptarget="integration_test/integration_test.dart" 90 | 91 | - name: Upload APKs to Firebase Test Lab 92 | run: | 93 | gcloud auth activate-service-account --key-file <(echo $GCLOUD_SERVICE_KEY) 94 | gcloud config set project $GCLOUD_PROJECT_ID 95 | gcloud firebase test android run \ 96 | --type instrumentation \ 97 | --app build/app/outputs/apk/debug/app-debug.apk \ 98 | --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk \ 99 | --timeout 5m \ 100 | 101 | -------------------------------------------------------------------------------- /.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 | *.env 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # The .vscode folder contains launch configuration and tasks you configure in 21 | # VS Code which you may wish to be included in version control, so this line 22 | # is commented out by default. 23 | .vscode/launch.json 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | **/ios/Flutter/.last_build_id 28 | .dart_tool/ 29 | .flutter-plugins 30 | .flutter-plugins-dependencies 31 | .packages 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | 47 | #Firebase Admin 48 | functions 49 | .firebaserc 50 | firebase.json 51 | 52 | #Code Coverage 53 | /coverage 54 | test/coverage_helper_test.dart 55 | 56 | # Environment file 57 | config.json -------------------------------------------------------------------------------- /.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: d9111f64021372856901a1fd5bfbc386cade3318 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: d9111f64021372856901a1fd5bfbc386cade3318 17 | base_revision: d9111f64021372856901a1fd5bfbc386cade3318 18 | - platform: android 19 | create_revision: d9111f64021372856901a1fd5bfbc386cade3318 20 | base_revision: d9111f64021372856901a1fd5bfbc386cade3318 21 | - platform: ios 22 | create_revision: d9111f64021372856901a1fd5bfbc386cade3318 23 | base_revision: d9111f64021372856901a1fd5bfbc386cade3318 24 | - platform: linux 25 | create_revision: d9111f64021372856901a1fd5bfbc386cade3318 26 | base_revision: d9111f64021372856901a1fd5bfbc386cade3318 27 | - platform: macos 28 | create_revision: d9111f64021372856901a1fd5bfbc386cade3318 29 | base_revision: d9111f64021372856901a1fd5bfbc386cade3318 30 | - platform: web 31 | create_revision: d9111f64021372856901a1fd5bfbc386cade3318 32 | base_revision: d9111f64021372856901a1fd5bfbc386cade3318 33 | - platform: windows 34 | create_revision: d9111f64021372856901a1fd5bfbc386cade3318 35 | base_revision: d9111f64021372856901a1fd5bfbc386cade3318 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 | -------------------------------------------------------------------------------- /.vscode/README.md: -------------------------------------------------------------------------------- 1 | Create a file launch.json and paste the following code, replacing `UPDATE_WITH_YOUR_ENDPOINT_URL_FROM_HASURA_CLOUD` with your own hasura cloud endpoint: 2 | 3 | ```json 4 | { 5 | // Use IntelliSense to learn about possible attributes. 6 | // Hover to view descriptions of existing attributes. 7 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 8 | "version": "0.2.0", 9 | "configurations": [ 10 | { 11 | "name": "financy_app", 12 | "request": "launch", 13 | "type": "dart", 14 | "toolArgs": [ 15 | "--dart-define=GRAPHQL_ENDPOINT=UPDATE_WITH_YOUR_ENDPOINT_URL_FROM_HASURA_CLOUD", 16 | ] 17 | }, 18 | { 19 | "name": "financy_app (profile mode)", 20 | "request": "launch", 21 | "type": "dart", 22 | "flutterMode": "profile", 23 | "toolArgs": [ 24 | "--dart-define=GRAPHQL_ENDPOINT=UPDATE_WITH_YOUR_ENDPOINT_URL_FROM_HASURA_CLOUD", 25 | ] 26 | }, 27 | { 28 | "name": "financy_app (release mode)", 29 | "request": "launch", 30 | "type": "dart", 31 | "flutterMode": "release", 32 | "toolArgs": [ 33 | "--dart-define=GRAPHQL_ENDPOINT=UPDATE_WITH_YOUR_ENDPOINT_URL_FROM_HASURA_CLOUD", 34 | ] 35 | } 36 | ] 37 | } 38 | ``` -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | // List of extensions which should be recommended for users of this workspace. 5 | "recommendations": [ 6 | "hzgood.dart-data-class-generator", 7 | "miquelddg.dart-barrel-file-generator", 8 | "luanpotter.dart-import", 9 | "donjayamanne.githistory", 10 | "redhat.vscode-yaml" 11 | ], 12 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 13 | "unwantedRecommendations": [] 14 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dartImport.fixOnSave": true, 3 | "dart.showIgnoreQuickFixes": false, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll": "explicit", 6 | "source.organizeImports": "explicit" 7 | }, 8 | "dart.lineLength": 80 9 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # financy_app 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /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 Exception("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 | 32 | def keystoreProperties = new Properties() 33 | def keystorePropertiesFile = rootProject.file('key.properties') 34 | if (keystorePropertiesFile.exists()) { 35 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 36 | } 37 | 38 | android { 39 | compileSdkVersion 33 40 | ndkVersion flutter.ndkVersion 41 | 42 | compileOptions { 43 | sourceCompatibility JavaVersion.VERSION_1_8 44 | targetCompatibility JavaVersion.VERSION_1_8 45 | } 46 | 47 | kotlinOptions { 48 | jvmTarget = '1.8' 49 | } 50 | 51 | sourceSets { 52 | main.java.srcDirs += 'src/main/kotlin' 53 | } 54 | 55 | defaultConfig { 56 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 57 | applicationId "dev.kaio.financy" 58 | // You can update the following values to match your application needs. 59 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 60 | minSdkVersion 21 61 | targetSdkVersion flutter.targetSdkVersion 62 | versionCode flutterVersionCode.toInteger() 63 | versionName flutterVersionName 64 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 65 | } 66 | 67 | signingConfigs { 68 | release { 69 | keyAlias keystoreProperties['keyAlias'] 70 | keyPassword keystoreProperties['keyPassword'] 71 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 72 | storePassword keystoreProperties['storePassword'] 73 | } 74 | } 75 | buildTypes { 76 | release { 77 | signingConfig signingConfigs.release 78 | } 79 | } 80 | 81 | namespace 'dev.kaio.financy' 82 | } 83 | 84 | flutter { 85 | source '../..' 86 | } 87 | 88 | dependencies { 89 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 90 | testImplementation "junit:junit:4.13.2" 91 | 92 | // https://developer.android.com/jetpack/androidx/releases/test/#1.2.0 93 | androidTestImplementation "androidx.test:runner:1.2.0" 94 | androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0" 95 | } 96 | -------------------------------------------------------------------------------- /android/app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "539092104395", 4 | "project_id": "financy-app-6650d", 5 | "storage_bucket": "financy-app-6650d.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:539092104395:android:676a145bab4c03b626d74e", 11 | "android_client_info": { 12 | "package_name": "dev.kaio.financy" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "539092104395-k1i8tab13cjpjcvebofv7gap065vs59e.apps.googleusercontent.com", 18 | "client_type": 3 19 | } 20 | ], 21 | "api_key": [ 22 | { 23 | "current_key": "AIzaSyCsMPPonxwD9ZstTs_5ha3wjBMpKSwmlvQ" 24 | } 25 | ], 26 | "services": { 27 | "appinvite_service": { 28 | "other_platform_oauth_client": [ 29 | { 30 | "client_id": "539092104395-k1i8tab13cjpjcvebofv7gap065vs59e.apps.googleusercontent.com", 31 | "client_type": 3 32 | }, 33 | { 34 | "client_id": "539092104395-5rijk44jomtu8u2blt7fdd4ltk63d1o6.apps.googleusercontent.com", 35 | "client_type": 2, 36 | "ios_info": { 37 | "bundle_id": "dev.kaio.financy" 38 | } 39 | } 40 | ] 41 | } 42 | } 43 | } 44 | ], 45 | "configuration_version": "1" 46 | } -------------------------------------------------------------------------------- /android/app/src/androidTest/java/dev/kaio/financy/financy_app/MainActivityTest.java: -------------------------------------------------------------------------------- 1 | package dev.kaio.financy; 2 | 3 | import androidx.test.rule.ActivityTestRule; 4 | import dev.flutter.plugins.integration_test.FlutterTestRunner; 5 | import org.junit.Rule; 6 | import org.junit.runner.RunWith; 7 | 8 | @RunWith(FlutterTestRunner.class) 9 | public class MainActivityTest { 10 | @Rule 11 | public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class, true, false); 12 | } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 17 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/dev/kaio/financy/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.kaio.financy 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-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.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.8.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:8.1.1' 10 | // START: FlutterFire Configuration 11 | classpath 'com.google.gms:google-services:4.3.15' 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.defaults.buildfeatures.buildconfig=true 5 | android.nonTransitiveRClass=false 6 | android.nonFinalResIds=false 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 10:47:10 BRT 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /assets/fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/assets/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/Inter-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/assets/fonts/Inter-ExtraBold.ttf -------------------------------------------------------------------------------- /assets/fonts/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/assets/fonts/Inter-Medium.ttf -------------------------------------------------------------------------------- /assets/fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/assets/fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/Inter-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/assets/fonts/Inter-SemiBold.ttf -------------------------------------------------------------------------------- /assets/images/check_your_email_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/assets/images/check_your_email_image.png -------------------------------------------------------------------------------- /assets/images/forgot_password_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/assets/images/forgot_password_image.png -------------------------------------------------------------------------------- /assets/images/man.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/assets/images/man.png -------------------------------------------------------------------------------- /assets/images/onboarding_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/assets/images/onboarding_image.png -------------------------------------------------------------------------------- /assets/images/sign_in_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/assets/images/sign_in_image.png -------------------------------------------------------------------------------- /assets/images/sign_up_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/assets/images/sign_up_image.png -------------------------------------------------------------------------------- /coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Create coverage import file 3 | file=test/coverage_helper_test.dart 4 | 5 | # Grant write permissions to the test/ directory 6 | chmod +w test/ 7 | 8 | # Import dart files with "package:financy_app/" prefix 9 | echo "// Helper file to make coverage work for all dart files\n" > "$file" 10 | echo "// ignore_for_file: unused_import, avoid_relative_lib_imports" >> "$file" 11 | 12 | # Find Dart files and generate import statements 13 | find lib -type f -name "*.dart" ! -path "lib/generated/*" | while read -r dart_file; do 14 | relative_path="${dart_file#lib/}" 15 | echo "import 'package:financy_app/$relative_path';" >> "$file" 16 | done 17 | 18 | echo -e "\nvoid main() {}" >> "$file" 19 | 20 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": [ 3 | { 4 | "source": "functions", 5 | "codebase": "default", 6 | "ignore": [ 7 | "node_modules", 8 | ".git", 9 | "firebase-debug.log", 10 | "firebase-debug.*.log" 11 | ] 12 | } 13 | ], 14 | "emulators": { 15 | "auth": { 16 | "port": 9099 17 | }, 18 | "functions": { 19 | "port": 5001 20 | }, 21 | "ui": { 22 | "enabled": true 23 | }, 24 | "singleProjectMode": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "serve": "firebase emulators:start --only functions", 6 | "shell": "firebase functions:shell", 7 | "start": "npm run shell", 8 | "deploy": "firebase deploy --only functions", 9 | "logs": "firebase functions:log" 10 | }, 11 | "engines": { 12 | "node": "18" 13 | }, 14 | "main": "index.js", 15 | "dependencies": { 16 | "dotenv": "^16.3.1", 17 | "firebase-admin": "^11.8.0", 18 | "firebase-functions": "^4.3.1", 19 | "graphql-request": "^6.1.0" 20 | }, 21 | "devDependencies": { 22 | "firebase-functions-test": "^3.1.0" 23 | }, 24 | "private": true 25 | } 26 | -------------------------------------------------------------------------------- /integration_test/integration_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:integration_test/integration_test.dart'; 4 | 5 | import 'robots/robot_extension.dart'; 6 | import 'utils.dart'; 7 | 8 | void main() { 9 | //Necessary to run integration tests 10 | IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 11 | 12 | //The app under test 13 | late Widget aut; 14 | const String email = 'user@tester.com'; 15 | const String password = '123456Abc'; 16 | 17 | setUp(() async { 18 | aut = await const Utils().createAppUnderTest(); 19 | }); 20 | 21 | testWidgets('Onboarding Test', (WidgetTester tester) async { 22 | await tester.pumpWidget(aut); 23 | 24 | await tester.onboarding.processOnboarding(); 25 | }); 26 | 27 | testWidgets('Failed Login Test', (WidgetTester tester) async { 28 | await tester.pumpWidget(aut); 29 | 30 | await tester.signInRobot.signInWithEmailAndPassword( 31 | email: email, 32 | password: password, 33 | shouldFail: true, 34 | ); 35 | }); 36 | 37 | testWidgets('Successful Login Test', (WidgetTester tester) async { 38 | await tester.pumpWidget(aut); 39 | 40 | await tester.signInRobot.signInWithEmailAndPassword( 41 | email: email, 42 | password: password, 43 | ); 44 | }); 45 | 46 | testWidgets('Logout test', (WidgetTester tester) async { 47 | await tester.pumpWidget(aut); 48 | 49 | await tester.signOutRobot.signOut(); 50 | }); 51 | 52 | testWidgets('Successful Forgot Password Test', (WidgetTester tester) async { 53 | await tester.pumpWidget(aut); 54 | 55 | await tester.forgotPasswordRobot.forgotPassword(email: email); 56 | }); 57 | 58 | testWidgets('Failed Forgot Password Test', (WidgetTester tester) async { 59 | await tester.pumpWidget(aut); 60 | 61 | await tester.forgotPasswordRobot.forgotPassword( 62 | email: email, 63 | shouldFail: true, 64 | ); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /integration_test/robots/forgot_password_robot.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/common/constants/constants.dart'; 2 | import 'package:financy_app/features/forgot_password/check_your_email_page.dart'; 3 | import 'package:financy_app/features/onboarding/onboarding.dart'; 4 | import 'package:financy_app/features/sign_in/sign_in.dart'; 5 | import 'package:financy_app/features/splash/splash.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | 8 | import 'robot_extension.dart'; 9 | 10 | class ForgotPasswordRobot { 11 | const ForgotPasswordRobot(this.tester); 12 | 13 | final WidgetTester tester; 14 | 15 | ///Remember to set [shouldFail] to `true` when login must fail. 16 | Future forgotPassword( 17 | {required String email, bool shouldFail = false}) async { 18 | final splashPage = find.byType(SplashPage); 19 | await tester.waitUntilFind(splashPage); 20 | expect(splashPage, findsOneWidget); 21 | 22 | final onboardingPage = find.byType(OnboardingPage); 23 | await tester.waitUntilFind(onboardingPage); 24 | expect(onboardingPage, findsOneWidget); 25 | 26 | final alreadyHaveAccountButton = 27 | find.byKey(Keys.onboardingAlreadyHaveAccountButton); 28 | 29 | expect(alreadyHaveAccountButton, findsOneWidget); 30 | await tester.tap(alreadyHaveAccountButton); 31 | 32 | final signInPage = find.byType(SignInPage); 33 | await tester.waitUntilFind(signInPage); 34 | expect(signInPage, findsOneWidget); 35 | 36 | final forgotPasswordButton = find.byKey(Keys.forgotPasswordButton); 37 | 38 | expect(forgotPasswordButton, findsOneWidget); 39 | 40 | await tester.tap(forgotPasswordButton); 41 | await tester.pumpAndSettle(); 42 | 43 | final emailField = find.byKey(Keys.forgotPasswordEmailField); 44 | await tester.waitUntilFind(emailField); 45 | expect(emailField, findsOneWidget); 46 | 47 | if (shouldFail) { 48 | await tester.enterText(emailField, 'wrong@email.com'); 49 | } else { 50 | await tester.enterText(emailField, email); 51 | } 52 | 53 | final sendLinkButton = find.byKey(Keys.forgotPasswordSendLinkButton); 54 | 55 | await tester.waitUntilFind(sendLinkButton); 56 | 57 | expect(sendLinkButton, findsOneWidget); 58 | await tester.tap(sendLinkButton); 59 | await tester.pumpAndSettle(); 60 | 61 | if (shouldFail) { 62 | final tryAgainText = find.text('Try again'); 63 | expect(tryAgainText, findsOneWidget); 64 | await tester.tap(tryAgainText); 65 | } else { 66 | final checkYourEmailPage = find.byType(CheckYourEmailPage); 67 | await tester.waitUntilFind(checkYourEmailPage); 68 | expect(checkYourEmailPage, findsOneWidget); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /integration_test/robots/onboarding_robot.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/common/constants/constants.dart'; 2 | import 'package:financy_app/features/onboarding/onboarding_page.dart'; 3 | import 'package:financy_app/features/sign_in/sign_in_page.dart'; 4 | import 'package:financy_app/features/sign_up/sign_up_page.dart'; 5 | import 'package:financy_app/features/splash/splash_page.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | 8 | import 'robot_extension.dart'; 9 | 10 | class OnboardingRobot { 11 | const OnboardingRobot(this.tester); 12 | 13 | final WidgetTester tester; 14 | 15 | Future processOnboarding() async { 16 | final splashPage = find.byType(SplashPage); 17 | await tester.waitUntilFind(splashPage); 18 | expect(splashPage, findsOneWidget); 19 | 20 | final onboardingPage = find.byType(OnboardingPage); 21 | await tester.waitUntilFind(onboardingPage); 22 | expect(onboardingPage, findsOneWidget); 23 | 24 | final getStartedButton = find.byKey(Keys.onboardingGetStartedButton); 25 | expect(getStartedButton, findsOneWidget); 26 | await tester.tap(getStartedButton); 27 | await tester.pumpAndSettle(); 28 | 29 | final signUpPage = find.byType(SignUpPage); 30 | await tester.waitUntilFind(signUpPage); 31 | expect(signUpPage, findsOneWidget); 32 | 33 | final signUpListView = find.byKey(Keys.signUpListView); 34 | 35 | final alreadyHaveAccountButton = 36 | find.byKey(Keys.signUpAlreadyHaveAccountButton); 37 | 38 | await tester.dragUntilFind( 39 | target: alreadyHaveAccountButton, 40 | scrollable: signUpListView, 41 | ); 42 | 43 | expect(alreadyHaveAccountButton, findsOneWidget); 44 | await tester.tap(alreadyHaveAccountButton); 45 | 46 | await tester.pumpAndSettle(); 47 | 48 | final signInPage = find.byType(SignInPage); 49 | await tester.waitUntilFind(signInPage); 50 | expect(signInPage, findsOneWidget); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /integration_test/robots/robot_extension.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | import 'forgot_password_robot.dart'; 4 | import 'onboarding_robot.dart'; 5 | import 'sign_in_robot.dart'; 6 | import 'sign_out_robot.dart'; 7 | 8 | extension RobotExtension on WidgetTester { 9 | OnboardingRobot get onboarding => OnboardingRobot(this); 10 | 11 | SignInRobot get signInRobot => SignInRobot(this); 12 | 13 | SignOutRobot get signOutRobot => SignOutRobot(this); 14 | 15 | ForgotPasswordRobot get forgotPasswordRobot => ForgotPasswordRobot(this); 16 | 17 | Future waitUntilFind( 18 | Finder finder, { 19 | Duration timeout = const Duration(seconds: 10), 20 | }) async { 21 | const interval = Duration(milliseconds: 1000); 22 | while (!any(finder)) { 23 | await pumpAndSettle(); 24 | timeout -= interval; 25 | if (timeout.inSeconds == 0) { 26 | throw TestFailure('Timeout waiting for $finder'); 27 | } 28 | } 29 | } 30 | 31 | Future dragUntilFind({ 32 | required Finder target, 33 | required Finder scrollable, 34 | }) async { 35 | await dragUntilVisible( 36 | target, 37 | scrollable, 38 | const Offset(0, -50), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /integration_test/robots/sign_in_robot.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/common/constants/keys.dart'; 2 | import 'package:financy_app/features/home/home_page_view.dart'; 3 | import 'package:financy_app/features/onboarding/onboarding_page.dart'; 4 | import 'package:financy_app/features/sign_in/sign_in_page.dart'; 5 | import 'package:financy_app/features/splash/splash_page.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | 8 | import 'robot_extension.dart'; 9 | 10 | class SignInRobot { 11 | const SignInRobot(this.tester); 12 | 13 | final WidgetTester tester; 14 | 15 | ///Remember to set [shouldFail] to `true` when login must fail. 16 | Future signInWithEmailAndPassword({ 17 | required String email, 18 | required String password, 19 | bool shouldFail = false, 20 | }) async { 21 | final splashPage = find.byType(SplashPage); 22 | await tester.waitUntilFind(splashPage); 23 | expect(splashPage, findsOneWidget); 24 | 25 | final onboardingPage = find.byType(OnboardingPage); 26 | await tester.waitUntilFind(onboardingPage); 27 | expect(onboardingPage, findsOneWidget); 28 | 29 | final alreadyHaveAccountButton = 30 | find.byKey(Keys.onboardingAlreadyHaveAccountButton); 31 | 32 | expect(alreadyHaveAccountButton, findsOneWidget); 33 | await tester.tap(alreadyHaveAccountButton); 34 | 35 | final signInPage = find.byType(SignInPage); 36 | await tester.waitUntilFind(signInPage); 37 | expect(signInPage, findsOneWidget); 38 | 39 | final signInListView = find.byKey(Keys.signInListView); 40 | 41 | final emailField = find.byKey(Keys.signInEmailField); 42 | await tester.waitUntilFind(emailField); 43 | expect(emailField, findsOneWidget); 44 | 45 | if (shouldFail) { 46 | await tester.enterText(emailField, 'wrong@email.com'); 47 | } else { 48 | await tester.enterText(emailField, email); 49 | } 50 | 51 | final passwordField = find.byKey(Keys.signInPasswordField); 52 | 53 | await tester.dragUntilFind( 54 | target: passwordField, 55 | scrollable: signInListView, 56 | ); 57 | 58 | expect(passwordField, findsOneWidget); 59 | 60 | if (shouldFail) { 61 | await tester.enterText(passwordField, 'wrongAbc123'); 62 | } else { 63 | await tester.enterText(passwordField, password); 64 | } 65 | 66 | final signInButton = find.byKey(Keys.signInButton); 67 | 68 | await tester.dragUntilFind( 69 | target: signInButton, 70 | scrollable: signInListView, 71 | ); 72 | 73 | expect(signInButton, findsOneWidget); 74 | await tester.tap(signInButton); 75 | await tester.pumpAndSettle(); 76 | 77 | if (shouldFail) { 78 | final tryAgainText = find.text('Try again'); 79 | expect(tryAgainText, findsOneWidget); 80 | await tester.tap(tryAgainText); 81 | } else { 82 | final homePage = find.byType(HomePageView); 83 | await tester.waitUntilFind(homePage); 84 | expect(homePage, findsOneWidget); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /integration_test/robots/sign_out_robot.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/common/constants/constants.dart'; 2 | import 'package:financy_app/features/home/home_page_view.dart'; 3 | import 'package:financy_app/features/onboarding/onboarding_page.dart'; 4 | import 'package:financy_app/features/profile/profile_page.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | import 'robot_extension.dart'; 8 | 9 | class SignOutRobot { 10 | const SignOutRobot(this.tester); 11 | 12 | final WidgetTester tester; 13 | 14 | Future signOut() async { 15 | final homePage = find.byType(HomePageView); 16 | await tester.waitUntilFind(homePage); 17 | expect(homePage, findsOneWidget); 18 | 19 | final profileButton = find.byKey(Keys.profilePageBottomAppBarItem); 20 | await tester.tap(profileButton); 21 | 22 | final profilePage = find.byType(ProfilePage); 23 | await tester.waitUntilFind(profilePage); 24 | expect(profilePage, findsOneWidget); 25 | 26 | final logoutButton = find.byKey(Keys.profilePagelogoutButton); 27 | expect(logoutButton, findsOneWidget); 28 | await tester.tap(logoutButton); 29 | 30 | final signInPage = find.byType(OnboardingPage); 31 | await tester.waitUntilFind(signInPage); 32 | expect(signInPage, findsOneWidget); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /integration_test/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/app.dart'; 2 | import 'package:financy_app/firebase_options.dart'; 3 | import 'package:financy_app/locator.dart'; 4 | import 'package:firebase_core/firebase_core.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | 8 | class Utils { 9 | const Utils(); 10 | 11 | /// Setup dependencies and returns [App] widget. 12 | /// Usually called in [setUp] method within main test. 13 | Future createAppUnderTest() async { 14 | await locator.reset(); 15 | 16 | await Firebase.initializeApp( 17 | options: DefaultFirebaseOptions.currentPlatform, 18 | ); 19 | 20 | setupDependencies(); 21 | 22 | await locator.allReady(); 23 | 24 | return const App(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /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 | 12.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, '12.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 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /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/AppIcon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon-20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "AppIcon-20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "AppIcon-29.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "AppIcon-29@2x.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "AppIcon-29@3x.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "AppIcon-40@2x.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "AppIcon-40@3x.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "AppIcon@2x.png", 47 | "idiom" : "iphone", 48 | "scale" : "2x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "AppIcon@3x.png", 53 | "idiom" : "iphone", 54 | "scale" : "3x", 55 | "size" : "60x60" 56 | }, 57 | { 58 | "filename" : "AppIcon-20~ipad.png", 59 | "idiom" : "ipad", 60 | "scale" : "1x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "AppIcon-20@2x~ipad.png", 65 | "idiom" : "ipad", 66 | "scale" : "2x", 67 | "size" : "20x20" 68 | }, 69 | { 70 | "filename" : "AppIcon-29~ipad.png", 71 | "idiom" : "ipad", 72 | "scale" : "1x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "AppIcon-29@2x~ipad.png", 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "29x29" 80 | }, 81 | { 82 | "filename" : "AppIcon-40~ipad.png", 83 | "idiom" : "ipad", 84 | "scale" : "1x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "AppIcon-40@2x~ipad.png", 89 | "idiom" : "ipad", 90 | "scale" : "2x", 91 | "size" : "40x40" 92 | }, 93 | { 94 | "filename" : "AppIcon~ipad.png", 95 | "idiom" : "ipad", 96 | "scale" : "1x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "AppIcon@2x~ipad.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "76x76" 104 | }, 105 | { 106 | "filename" : "AppIcon-83.5@2x~ipad.png", 107 | "idiom" : "ipad", 108 | "scale" : "2x", 109 | "size" : "83.5x83.5" 110 | }, 111 | { 112 | "filename" : "AppIcon-60@2x~car.png", 113 | "idiom" : "car", 114 | "scale" : "2x", 115 | "size" : "60x60" 116 | }, 117 | { 118 | "filename" : "AppIcon-60@3x~car.png", 119 | "idiom" : "car", 120 | "scale" : "3x", 121 | "size" : "60x60" 122 | }, 123 | { 124 | "filename" : "AppIcon~ios-marketing.png", 125 | "idiom" : "ios-marketing", 126 | "scale" : "1x", 127 | "size" : "1024x1024" 128 | } 129 | ], 130 | "info" : { 131 | "author" : "xcode", 132 | "version" : 1 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devkaio/financy_app/bb00be559da26c5a370428708434011bb56cb30e/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/ExportOptions.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | destination 6 | export 7 | manageAppVersionAndBuildNumber 8 | 9 | method 10 | app-store 11 | provisioningProfiles 12 | 13 | dev.kaio.financy 14 | financy_prod 15 | 16 | signingCertificate 17 | AFD38E00153FBA4E0A3DC2459C7140BA8D043870 18 | signingStyle 19 | manual 20 | stripSwiftSymbols 21 | 22 | teamID 23 | 2HSHWBAZS4 24 | uploadSymbols 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /ios/Runner/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 539092104395-5rijk44jomtu8u2blt7fdd4ltk63d1o6.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.539092104395-5rijk44jomtu8u2blt7fdd4ltk63d1o6 9 | API_KEY 10 | AIzaSyDh3TdIA1yIJV1INRPwXhVQUlu44ctJSJ8 11 | GCM_SENDER_ID 12 | 539092104395 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | dev.kaio.financy 17 | PROJECT_ID 18 | financy-app-6650d 19 | STORAGE_BUCKET 20 | financy-app-6650d.appspot.com 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:539092104395:ios:f1326e9eb37f99f126d74e 33 | 34 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Financy App 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | financy_app 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 | DART_DEFINES 49 | $(DART_DEFINES) 50 | UIApplicationSupportsIndirectInputEvents 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /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:539092104395:ios:f1326e9eb37f99f126d74e", 5 | "FIREBASE_PROJECT_ID": "financy-app-6650d", 6 | "GCM_SENDER_ID": "539092104395" 7 | } -------------------------------------------------------------------------------- /lib/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'common/constants/constants.dart'; 4 | import 'common/models/models.dart'; 5 | import 'common/themes/default_theme.dart'; 6 | import 'features/forgot_password/check_your_email_page.dart'; 7 | import 'features/forgot_password/forgot_password_page.dart'; 8 | import 'features/home/home.dart'; 9 | import 'features/onboarding/onboarding.dart'; 10 | import 'features/profile/profile.dart'; 11 | import 'features/sign_in/sign_in.dart'; 12 | import 'features/sign_up/sign_up.dart'; 13 | import 'features/splash/splash.dart'; 14 | import 'features/stats/stats.dart'; 15 | import 'features/transactions/transactions.dart'; 16 | import 'features/wallet/wallet.dart'; 17 | 18 | class App extends StatelessWidget { 19 | const App({super.key}); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return MaterialApp( 24 | debugShowCheckedModeBanner: false, 25 | theme: CustomTheme().defaultTheme, 26 | initialRoute: NamedRoute.splash, 27 | routes: { 28 | NamedRoute.initial: (context) => const OnboardingPage(), 29 | NamedRoute.splash: (context) => const SplashPage(), 30 | NamedRoute.signUp: (context) => const SignUpPage(), 31 | NamedRoute.signIn: (context) => const SignInPage(), 32 | NamedRoute.home: (context) => const HomePageView(), 33 | NamedRoute.stats: (context) => const StatsPage(), 34 | NamedRoute.wallet: (context) => const WalletPage(), 35 | NamedRoute.profile: (context) => const ProfilePage(), 36 | NamedRoute.transaction: (context) { 37 | final args = ModalRoute.of(context)?.settings.arguments; 38 | return TransactionPage( 39 | transaction: args != null ? args as TransactionModel : null, 40 | ); 41 | }, 42 | NamedRoute.forgotPassword: (context) => const ForgotPasswordPage(), 43 | NamedRoute.checkYourEmail: (context) => const CheckYourEmailPage(), 44 | }, 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/common/constants/app_colors.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | class AppColors { 4 | AppColors._(); 5 | 6 | static const Color greenOne = Color(0xFF438883); 7 | static const Color greenTwo = Color(0xFF63B5AF); 8 | static const List greenGradient = [ 9 | Color(0xFF63B5AF), 10 | Color(0xFF438883), 11 | ]; 12 | static const List greyGradient = [ 13 | Color(0xFFB5B5B5), 14 | Color(0xFF7F7F7F), 15 | ]; 16 | static const Color white = Color(0xFFFFFFFF); 17 | static const Color iceWhite = Color(0xFFEEF8F7); 18 | static const Color antiFlashWhite = Color(0xFFF0F6F5); 19 | static const Color blackGrey = Color(0xFF222222); 20 | static const Color darkGrey = Color(0xFF444444); 21 | static const Color grey = Color(0xFF666666); 22 | static const Color lightGrey = Color(0xFFAAAAAA); 23 | static const Color error = Color(0xFFF44336); 24 | static const Color green = Color(0xFF438883); 25 | static const Color darkGreen = Color(0xFF2F7E79); 26 | static const Color income = Color(0xFF25A969); 27 | static const Color outcome = Color(0xFFF95B51); 28 | static const Color notification = Color(0xFFFFAB7B); 29 | } 30 | -------------------------------------------------------------------------------- /lib/common/constants/app_text_styles.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/painting.dart'; 2 | 3 | class AppTextStyles { 4 | AppTextStyles._(); 5 | 6 | static const TextStyle bigText50 = TextStyle( 7 | fontFamily: 'Inter', 8 | fontSize: 50.0, 9 | fontWeight: FontWeight.w700, 10 | ); 11 | 12 | static const TextStyle mediumText36 = TextStyle( 13 | fontFamily: 'Inter', 14 | fontSize: 36.0, 15 | fontWeight: FontWeight.w700, 16 | ); 17 | 18 | static const TextStyle mediumText30 = TextStyle( 19 | fontFamily: 'Inter', 20 | fontSize: 30.0, 21 | fontWeight: FontWeight.w700, 22 | ); 23 | 24 | static const TextStyle mediumText16w500 = TextStyle( 25 | fontFamily: 'Inter', 26 | fontSize: 16.0, 27 | fontWeight: FontWeight.w500, 28 | ); 29 | 30 | static const TextStyle mediumText16w600 = TextStyle( 31 | fontFamily: 'Inter', 32 | fontSize: 16.0, 33 | fontWeight: FontWeight.w600, 34 | ); 35 | 36 | static const TextStyle mediumText18 = TextStyle( 37 | fontFamily: 'Inter', 38 | fontSize: 18.0, 39 | fontWeight: FontWeight.w600, 40 | ); 41 | 42 | static const TextStyle mediumText20 = TextStyle( 43 | fontFamily: 'Inter', 44 | fontSize: 20.0, 45 | fontWeight: FontWeight.w600, 46 | ); 47 | 48 | static const TextStyle smallText = TextStyle( 49 | fontFamily: 'Inter', 50 | fontSize: 14.0, 51 | fontWeight: FontWeight.w500, 52 | ); 53 | static const TextStyle smallText13 = TextStyle( 54 | fontFamily: 'Inter', 55 | fontSize: 13.0, 56 | fontWeight: FontWeight.w400, 57 | ); 58 | 59 | static const TextStyle inputLabelText = TextStyle( 60 | fontFamily: 'Inter', 61 | fontSize: 14.0, 62 | fontWeight: FontWeight.w400, 63 | ); 64 | 65 | static const TextStyle inputText = TextStyle( 66 | fontFamily: 'Inter', 67 | fontSize: 14.0, 68 | fontWeight: FontWeight.w500, 69 | ); 70 | 71 | static const TextStyle inputHintText = TextStyle( 72 | fontFamily: 'Inter', 73 | fontSize: 12.0, 74 | fontWeight: FontWeight.w400, 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /lib/common/constants/constants.dart: -------------------------------------------------------------------------------- 1 | export 'app_colors.dart'; 2 | export 'app_text_styles.dart'; 3 | export 'date.dart'; 4 | export 'keys.dart'; 5 | export 'mutations.dart'; 6 | export 'queries.dart'; 7 | export 'routes.dart'; 8 | -------------------------------------------------------------------------------- /lib/common/constants/date.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/common/extensions/extensions.dart'; 2 | 3 | int get hoursInDay => 24; 4 | int get daysInWeek => 7; 5 | int get weeksInMonth => DateTime.now().weeksInMonth; 6 | int get monthsInYear => 12; 7 | -------------------------------------------------------------------------------- /lib/common/constants/environment.dart: -------------------------------------------------------------------------------- 1 | class Environment { 2 | const Environment(); 3 | 4 | String get graphqlEndpoint => 5 | const String.fromEnvironment('GRAPHQL_ENDPOINT'); 6 | } 7 | -------------------------------------------------------------------------------- /lib/common/constants/keys.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | class Keys { 4 | Keys._(); 5 | 6 | // Onboarding page 7 | static const onboardingGetStartedButton = 8 | Key('onboarding_get_started_button'); 9 | static const onboardingAlreadyHaveAccountButton = 10 | Key('onboarding_already_have_account_button'); 11 | 12 | // Sign up page 13 | static const signUpListView = Key('sign_up_listview'); 14 | static const signUpNameField = Key('sign_up_name_field'); 15 | static const signUpEmailField = Key('sign_up_email_field'); 16 | static const signUpPasswordField = Key('sign_up_password_field'); 17 | static const signUpConfirmPasswordField = 18 | Key('sign_up_confirm_password_field'); 19 | static const signUpButton = Key('sign_up_button'); 20 | static const signUpShowHidePasswordButton = 21 | Key('sign_up_show_hide_password_button'); 22 | static const signUpAlreadyHaveAccountButton = 23 | Key('sign_up_already_have_account_button'); 24 | 25 | // Sign in page 26 | static const signInListView = Key('sign_in_listview'); 27 | static const signInEmailField = Key('sign_in_email_field'); 28 | static const signInPasswordField = Key('sign_in_password_field'); 29 | static const signInButton = Key('sign_in_button'); 30 | static const signInShowHidePasswordButton = 31 | Key('sign_in_show_hide_password_button'); 32 | static const signInDontHaveAccountButton = 33 | Key('sign_in_dont_have_account_button'); 34 | 35 | // Forgot password page 36 | static const forgotPasswordButton = Key('forgot_password_button'); 37 | static const forgotPasswordEmailField = Key('forgot_password_email_field'); 38 | static const forgotPasswordSendLinkButton = 39 | Key('forgot_password_send_link_button'); 40 | 41 | // App bottom bar items 42 | static const homePageBottomAppBarItem = Key('home_page_bottom_app_bar_item'); 43 | static const statsPageBottomAppBarItem = 44 | Key('stats_page_bottom_app_bar_item'); 45 | static const walletPageBottomAppBarItem = 46 | Key('wallet_page_bottom_app_bar_item'); 47 | static const profilePageBottomAppBarItem = 48 | Key('profile_page_bottom_app_bar_item'); 49 | 50 | // Profile page 51 | static const profilePagelogoutButton = Key('profile_page_logout_button'); 52 | } 53 | -------------------------------------------------------------------------------- /lib/common/constants/mutations.dart: -------------------------------------------------------------------------------- 1 | abstract class Mutations { 2 | static const String mAddNewTransaction = r""" 3 | mutation addNewTransaction($id: uuid!, $category: String!, $date: timestamptz!, $created_at: timestamptz!, $description: String!, $status: Boolean!, $value: numeric!, $user_id: String!) { 4 | insert_transaction_one(object: {id: $id, date: $date, created_at: $created_at, description: $description, status: $status, value: $value, category: $category, user_id: $user_id}) { 5 | category 6 | created_at 7 | date 8 | description 9 | id 10 | status 11 | value 12 | user_id 13 | } 14 | }"""; 15 | 16 | static const String mUpdateTransaction = r""" 17 | mutation updateTransaction($id: uuid!, $category: String!, $date: timestamptz!, $description: String!, $value: numeric!, $status: Boolean!, $created_at: timestamptz!) { 18 | update_transaction_by_pk(pk_columns: {id: $id}, _set: { category: $category, date: $date, description: $description, status: $status, value: $value, created_at: $created_at}) { 19 | category 20 | created_at 21 | date 22 | description 23 | id 24 | status 25 | value 26 | user_id 27 | } 28 | } 29 | """; 30 | 31 | static const String mDeleteTransaction = r""" 32 | mutation deleteTransaction($id: uuid!) { 33 | delete_transaction(where: {id: {_eq: $id}}) { 34 | affected_rows 35 | } 36 | } 37 | """; 38 | } 39 | -------------------------------------------------------------------------------- /lib/common/constants/queries.dart: -------------------------------------------------------------------------------- 1 | abstract class Queries { 2 | static const String qGetBalances = r""" 3 | query getBalances { 4 | totalBalance: transaction_aggregate { 5 | aggregate { 6 | sum { 7 | value 8 | } 9 | } 10 | } 11 | totalIncome: transaction_aggregate(where: {value: {_gt: "0"}}) { 12 | aggregate { 13 | sum { 14 | value 15 | } 16 | } 17 | } 18 | totalOutcome: transaction_aggregate(where: {value: {_lt: "0"}}) { 19 | aggregate { 20 | sum { 21 | value 22 | } 23 | } 24 | } 25 | } 26 | """; 27 | 28 | static const String qGetTrasactions = r""" 29 | query getTransactions($limit: Int, $offset: Int) { 30 | transaction(limit: $limit, order_by: {date: desc}, offset: $offset) { 31 | category 32 | created_at 33 | date 34 | description 35 | id 36 | status 37 | user_id 38 | value 39 | } 40 | } 41 | """; 42 | } 43 | -------------------------------------------------------------------------------- /lib/common/constants/routes.dart: -------------------------------------------------------------------------------- 1 | class NamedRoute { 2 | NamedRoute._(); 3 | 4 | static const String initial = "/"; 5 | static const String splash = "/splash"; 6 | static const String signUp = "/sign_up"; 7 | static const String signIn = "/sign_in"; 8 | static const String home = "/home"; 9 | static const String stats = "/stats"; 10 | static const String wallet = "/wallet"; 11 | static const String profile = "/profile"; 12 | static const String transaction = "/transaction"; 13 | static const String forgotPassword = "/forgot-password"; 14 | static const String checkYourEmail = "/check-your-email"; 15 | } 16 | -------------------------------------------------------------------------------- /lib/common/data/data.dart: -------------------------------------------------------------------------------- 1 | export 'data_result.dart'; 2 | export 'exceptions.dart'; 3 | -------------------------------------------------------------------------------- /lib/common/data/data_result.dart: -------------------------------------------------------------------------------- 1 | import 'exceptions.dart'; 2 | 3 | abstract class DataResult { 4 | const DataResult(); 5 | 6 | static DataResult failure(Failure failure) => _FailureResult(failure); 7 | static DataResult success(S data) => _SuccessResult(data); 8 | 9 | Failure? get error => fold( 10 | (error) => error, 11 | (data) => null, 12 | ); 13 | 14 | S? get data => fold( 15 | (error) => null, 16 | (data) => data, 17 | ); 18 | 19 | T fold( 20 | T Function(Failure error) fnFailure, 21 | T Function(S data) fnData, 22 | ); 23 | } 24 | 25 | class _SuccessResult extends DataResult { 26 | const _SuccessResult(this._value); 27 | final S _value; 28 | 29 | @override 30 | T fold( 31 | T Function(Failure error) fnFailure, 32 | T Function(S data) fnData, 33 | ) { 34 | return fnData(_value); 35 | } 36 | } 37 | 38 | class _FailureResult extends DataResult { 39 | const _FailureResult(this._value); 40 | 41 | final Failure _value; 42 | 43 | @override 44 | T fold( 45 | T Function(Failure error) fnFailure, 46 | T Function(S data) fnData, 47 | ) { 48 | return fnFailure(_value); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/common/extensions/date_formatter.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | extension DateTimeFormatter on DateTime { 4 | String get toText { 5 | final now = DateTime.now(); 6 | 7 | final startOfToday = DateTime(now.year, now.month, now.day); 8 | final endOfToday = DateTime(now.year, now.month, now.day, 23, 59, 59); 9 | 10 | if (isAfter(startOfToday.add(const Duration(days: 1))) && 11 | isBefore(endOfToday.add(const Duration(days: 1)))) { 12 | return 'Tomorrow'; 13 | } else if (isAfter(startOfToday) && isBefore(endOfToday)) { 14 | return 'Today'; 15 | } else if (isAfter(startOfToday.subtract(const Duration(days: 1))) && 16 | isBefore(endOfToday.subtract(const Duration(days: 1)))) { 17 | return 'Yesterday'; 18 | } else { 19 | return DateFormat('EEE, MMM d, ' 'yy').format(this); 20 | } 21 | } 22 | 23 | String get formatISOTime { 24 | var duration = timeZoneOffset; 25 | if (duration.isNegative) { 26 | return ("${toIso8601String().replaceAll('Z', '-')}${duration.inHours.toString().padLeft(2, '0')}:${(duration.inMinutes - (duration.inHours * 60)).toString().padLeft(2, '0')}"); 27 | } else { 28 | return ("${toIso8601String().replaceAll('Z', '+')}${duration.inHours.toString().padLeft(2, '0')}:${(duration.inMinutes - (duration.inHours * 60)).toString().padLeft(2, '0')}"); 29 | } 30 | } 31 | 32 | /// Returns the date in the format yyyy-MM-dd 33 | String get yMd { 34 | return DateFormat('yyyy-MM-dd').format(this); 35 | } 36 | 37 | int get weeksInMonth { 38 | DateTime firstDayOfMonth = DateTime(year, month, 1); 39 | DateTime lastDayOfMonth = DateTime(year, month + 1, 0); 40 | 41 | // Calculate the number of days in the month 42 | int daysInMonth = lastDayOfMonth.day; 43 | 44 | // Calculate the weekday of the first day of the month (0 = Sunday, 6 = Saturday) 45 | int firstDayWeekday = firstDayOfMonth.weekday; 46 | 47 | // Calculate the number of days to complete the last week of the month 48 | int remainingDays = 7 - (firstDayWeekday - 1); 49 | 50 | // Calculate the number of full weeks in the month 51 | int fullWeeks = (daysInMonth - remainingDays) ~/ 7; 52 | 53 | // Calculate the total number of weeks in the month 54 | int totalWeeks = fullWeeks + 1; // Add one for the last week 55 | 56 | return totalWeeks; 57 | } 58 | 59 | int get week { 60 | final firstDayOfMonth = DateTime(year, month, 1); 61 | final daysOffset = (weekday - firstDayOfMonth.weekday + 1 + 7) % 7; 62 | return (day + daysOffset - 1) ~/ 7 + 1; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/common/extensions/extensions.dart: -------------------------------------------------------------------------------- 1 | export 'date_formatter.dart'; 2 | export 'page_controller_ext.dart'; 3 | export 'sizes.dart'; 4 | export 'types_ext.dart'; 5 | -------------------------------------------------------------------------------- /lib/common/extensions/page_controller_ext.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | enum BottomAppBarItem { home, stats, wallet, profile } 4 | 5 | extension PageControllerExt on PageController { 6 | static int _selectedIndex = 0; 7 | 8 | int get selectedBottomAppBarItemIndex { 9 | final newIndex = page ?? _selectedIndex; 10 | if (newIndex > 1) { 11 | return (newIndex + 1).toInt(); 12 | } 13 | return newIndex.toInt(); 14 | } 15 | 16 | set setBottomAppBarItemIndex(int newIndex) { 17 | _selectedIndex = newIndex; 18 | } 19 | 20 | void navigateTo(BottomAppBarItem item) { 21 | switch (item) { 22 | case BottomAppBarItem.home: 23 | jumpToPage(BottomAppBarItem.home.index); 24 | break; 25 | case BottomAppBarItem.stats: 26 | jumpToPage(BottomAppBarItem.stats.index); 27 | break; 28 | case BottomAppBarItem.wallet: 29 | jumpToPage(BottomAppBarItem.wallet.index); 30 | break; 31 | case BottomAppBarItem.profile: 32 | jumpToPage(BottomAppBarItem.profile.index); 33 | break; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/common/extensions/sizes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Sizes { 4 | //construtor privado 5 | Sizes._(); 6 | 7 | double _width = 0; 8 | double _height = 0; 9 | 10 | //valor inicial baseado no protótipo 11 | static const Size _designSize = Size(414.0, 896.0); 12 | 13 | static final Sizes _instance = Sizes._(); 14 | 15 | //construtor singleton 16 | factory Sizes() => _instance; 17 | 18 | double get width => _width; 19 | double get height => _height; 20 | 21 | //método de configuração inicial 22 | static void init( 23 | BuildContext context, { 24 | Size designSize = _designSize, 25 | }) { 26 | //verifica se existe dados de MediaQuery 27 | final deviceData = MediaQuery.maybeOf(context); 28 | 29 | //caso não exista, recebe o valor inicial do protótipo 30 | final deviceSize = deviceData?.size ?? _designSize; 31 | 32 | //atualiza getters 33 | _instance._height = deviceSize.height; 34 | _instance._width = deviceSize.width; 35 | } 36 | } 37 | 38 | extension SizesExt on num { 39 | ///Calcula o valor proporcional baseado na largura do dispositivo 40 | ///em relação ao protótipo. 41 | double get w { 42 | return (this * Sizes._instance._width) / Sizes._designSize.width; 43 | } 44 | 45 | ///Calcula o valor proporcional baseado na altura do dispositivo 46 | ///em relação ao protótipo. 47 | double get h { 48 | return (this * Sizes._instance._height) / Sizes._designSize.height; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/common/extensions/types_ext.dart: -------------------------------------------------------------------------------- 1 | extension BoolExt on bool { 2 | int toInt() => this ? 1 : 0; 3 | } 4 | 5 | extension StringExt on String { 6 | bool toBool() => this == 'true' ? true : false; 7 | 8 | String capitalize() { 9 | if (isEmpty) { 10 | return this; 11 | } 12 | 13 | List words = split(' '); 14 | 15 | List capitalizedWords = words.map((word) { 16 | if (word.isEmpty) { 17 | return word; 18 | } 19 | String firstLetter = word[0].toUpperCase(); 20 | String restOfWord = word.substring(1).toLowerCase(); 21 | return '$firstLetter$restOfWord'; 22 | }).toList(); 23 | 24 | return capitalizedWords.join(' '); 25 | } 26 | 27 | String get firstWord => split(' ').first; 28 | } 29 | -------------------------------------------------------------------------------- /lib/common/features/balance/balance.dart: -------------------------------------------------------------------------------- 1 | export 'balance_controller.dart'; 2 | export 'balance_state.dart'; 3 | -------------------------------------------------------------------------------- /lib/common/features/balance/balance_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | import '../../../repositories/repositories.dart'; 4 | import '../../models/models.dart'; 5 | import 'balance_state.dart'; 6 | 7 | class BalanceController extends ChangeNotifier { 8 | BalanceController({ 9 | required this.transactionRepository, 10 | }); 11 | 12 | final TransactionRepository transactionRepository; 13 | 14 | BalanceState _state = BalanceStateInitial(); 15 | 16 | BalanceState get state => _state; 17 | 18 | BalancesModel _balances = BalancesModel( 19 | totalIncome: 0, 20 | totalOutcome: 0, 21 | totalBalance: 0, 22 | ); 23 | BalancesModel get balances => _balances; 24 | 25 | void _changeState(BalanceState newState) { 26 | _state = newState; 27 | notifyListeners(); 28 | } 29 | 30 | Future getBalances() async { 31 | _changeState(BalanceStateLoading()); 32 | 33 | final result = await transactionRepository.getBalances(); 34 | 35 | result.fold( 36 | (error) => _changeState(BalanceStateError()), 37 | (data) { 38 | _balances = data; 39 | 40 | _changeState(BalanceStateSuccess()); 41 | }, 42 | ); 43 | } 44 | 45 | Future updateBalance( 46 | {TransactionModel? oldTransaction, 47 | required TransactionModel newTransaction}) async { 48 | final result = await transactionRepository.updateBalance( 49 | oldTransaction: oldTransaction, 50 | newTransaction: newTransaction, 51 | ); 52 | 53 | result.fold( 54 | (error) => _changeState(BalanceStateError()), 55 | (data) { 56 | _balances = data; 57 | _changeState(BalanceStateSuccess()); 58 | }, 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/common/features/balance/balance_state.dart: -------------------------------------------------------------------------------- 1 | abstract class BalanceState {} 2 | 3 | class BalanceStateInitial extends BalanceState {} 4 | 5 | class BalanceStateLoading extends BalanceState {} 6 | 7 | class BalanceStateSuccess extends BalanceState {} 8 | 9 | class BalanceStateError extends BalanceState {} 10 | -------------------------------------------------------------------------------- /lib/common/features/transaction/transaction.dart: -------------------------------------------------------------------------------- 1 | export 'transaction_controller.dart'; 2 | export 'transaction_state.dart'; 3 | -------------------------------------------------------------------------------- /lib/common/features/transaction/transaction_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | import '../../../repositories/repositories.dart'; 4 | import '../../../services/services.dart'; 5 | import '../../models/models.dart'; 6 | import 'transaction_state.dart'; 7 | 8 | class TransactionController extends ChangeNotifier { 9 | TransactionController({ 10 | required this.transactionRepository, 11 | required this.secureStorageService, 12 | }); 13 | 14 | final SecureStorageService secureStorageService; 15 | final TransactionRepository transactionRepository; 16 | 17 | TransactionState _state = TransactionStateInitial(); 18 | 19 | TransactionState get state => _state; 20 | 21 | void _changeState(TransactionState newState) { 22 | _state = newState; 23 | notifyListeners(); 24 | } 25 | 26 | Future addTransaction(TransactionModel transaction) async { 27 | _changeState(TransactionStateLoading()); 28 | 29 | final data = await secureStorageService.readOne(key: 'CURRENT_USER'); 30 | final user = UserModel.fromJson(data ?? ''); 31 | final result = await transactionRepository.addTransaction( 32 | transaction: transaction, 33 | userId: user.id!, 34 | ); 35 | 36 | result.fold( 37 | (error) => _changeState(TransactionStateError(message: error.message)), 38 | (data) => _changeState(TransactionStateSuccess()), 39 | ); 40 | } 41 | 42 | Future updateTransaction(TransactionModel transaction) async { 43 | _changeState(TransactionStateLoading()); 44 | final result = await transactionRepository.updateTransaction(transaction); 45 | 46 | result.fold( 47 | (error) => _changeState(TransactionStateError(message: error.message)), 48 | (data) => _changeState(TransactionStateSuccess()), 49 | ); 50 | } 51 | 52 | Future deleteTransaction(TransactionModel transaction) async { 53 | _changeState(TransactionStateLoading()); 54 | final result = await transactionRepository.deleteTransaction(transaction); 55 | 56 | result.fold( 57 | (error) => _changeState(TransactionStateError(message: error.message)), 58 | (data) => _changeState(TransactionStateSuccess()), 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/common/features/transaction/transaction_state.dart: -------------------------------------------------------------------------------- 1 | abstract class TransactionState {} 2 | 3 | class TransactionStateInitial extends TransactionState {} 4 | 5 | class TransactionStateLoading extends TransactionState {} 6 | 7 | class TransactionStateSuccess extends TransactionState {} 8 | 9 | class TransactionStateError extends TransactionState { 10 | TransactionStateError({required this.message}); 11 | 12 | final String message; 13 | } 14 | -------------------------------------------------------------------------------- /lib/common/models/balances_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class BalancesModel { 4 | final double totalIncome; 5 | final double totalOutcome; 6 | final double totalBalance; 7 | BalancesModel({ 8 | required this.totalIncome, 9 | required this.totalOutcome, 10 | required this.totalBalance, 11 | }); 12 | 13 | Map toMap() { 14 | return { 15 | 'total_income': totalIncome, 16 | 'total_outcome': totalOutcome, 17 | 'total_balance': totalBalance, 18 | }; 19 | } 20 | 21 | factory BalancesModel.fromMap(Map map) { 22 | if (map.containsKey('__typename')) { 23 | return BalancesModel( 24 | totalIncome: double.tryParse( 25 | map['totalIncome']['aggregate']['sum']['value'].toString()) ?? 26 | 0, 27 | totalOutcome: double.tryParse( 28 | map['totalOutcome']['aggregate']['sum']['value'].toString()) ?? 29 | 0, 30 | totalBalance: double.tryParse( 31 | map['totalBalance']['aggregate']['sum']['value'].toString()) ?? 32 | 0, 33 | ); 34 | } 35 | 36 | if (map.containsKey('data')) { 37 | final localMap = (map['data'] as List).first; 38 | return BalancesModel( 39 | totalIncome: double.tryParse(localMap['total_income'].toString()) ?? 0, 40 | totalOutcome: 41 | double.tryParse(localMap['total_outcome'].toString()) ?? 0, 42 | totalBalance: 43 | double.tryParse(localMap['total_balance'].toString()) ?? 0, 44 | ); 45 | } 46 | 47 | return BalancesModel( 48 | totalIncome: double.tryParse(map['total_income'].toString()) ?? 0, 49 | totalOutcome: double.tryParse(map['total_outcome'].toString()) ?? 0, 50 | totalBalance: double.tryParse(map['total_balance'].toString()) ?? 0, 51 | ); 52 | } 53 | 54 | String toJson() => json.encode(toMap()); 55 | 56 | factory BalancesModel.fromJson(String source) => 57 | BalancesModel.fromMap(json.decode(source) as Map); 58 | 59 | BalancesModel copyWith({ 60 | double? totalIncome, 61 | double? totalOutcome, 62 | double? totalBalance, 63 | }) { 64 | return BalancesModel( 65 | totalIncome: totalIncome ?? this.totalIncome, 66 | totalOutcome: totalOutcome ?? this.totalOutcome, 67 | totalBalance: totalBalance ?? this.totalBalance, 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/common/models/models.dart: -------------------------------------------------------------------------------- 1 | export 'balances_model.dart'; 2 | export 'transaction_model.dart'; 3 | export 'user_model.dart'; 4 | -------------------------------------------------------------------------------- /lib/common/models/user_model.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | import 'dart:convert'; 3 | 4 | class UserModel { 5 | final String? id; 6 | final String? name; 7 | final String? email; 8 | final String? password; 9 | 10 | UserModel({ 11 | this.id, 12 | this.name, 13 | this.email, 14 | this.password, 15 | }); 16 | 17 | Map toMap() { 18 | return { 19 | 'id': id, 20 | 'name': name, 21 | 'email': email, 22 | 'password': password, 23 | }; 24 | } 25 | 26 | factory UserModel.fromMap(Map map) { 27 | return UserModel( 28 | id: map['id'] != null ? map['id'] as String : null, 29 | name: map['name'] != null ? map['name'] as String : null, 30 | email: map['email'] != null ? map['email'] as String : null, 31 | password: map['password'] != null ? map['password'] as String : null, 32 | ); 33 | } 34 | 35 | String toJson() => json.encode(toMap()); 36 | 37 | factory UserModel.fromJson(String source) => 38 | UserModel.fromMap(json.decode(source) as Map); 39 | 40 | UserModel copyWith({ 41 | String? id, 42 | String? name, 43 | String? email, 44 | String? password, 45 | }) { 46 | return UserModel( 47 | id: id ?? this.id, 48 | name: name ?? this.name, 49 | email: email ?? this.email, 50 | password: password ?? this.password, 51 | ); 52 | } 53 | 54 | @override 55 | bool operator ==(covariant UserModel other) { 56 | if (identical(this, other)) return true; 57 | 58 | return other.id == id && 59 | other.name == name && 60 | other.email == email && 61 | other.password == password; 62 | } 63 | 64 | @override 65 | int get hashCode { 66 | return id.hashCode ^ name.hashCode ^ email.hashCode ^ password.hashCode; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/common/themes/default_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../constants/app_colors.dart'; 4 | import '../constants/app_text_styles.dart'; 5 | 6 | class CustomTheme { 7 | CustomTheme._(); 8 | 9 | factory CustomTheme() { 10 | return CustomTheme._(); 11 | } 12 | 13 | ThemeData get defaultTheme { 14 | const defaultBorder = OutlineInputBorder( 15 | borderSide: BorderSide( 16 | color: AppColors.greenOne, 17 | ), 18 | ); 19 | 20 | return ThemeData( 21 | useMaterial3: false, 22 | colorScheme: const ColorScheme.light( 23 | primary: AppColors.darkGreen, 24 | ), 25 | floatingActionButtonTheme: const FloatingActionButtonThemeData( 26 | foregroundColor: AppColors.iceWhite, 27 | backgroundColor: AppColors.green, 28 | ), 29 | textButtonTheme: TextButtonThemeData( 30 | style: TextButton.styleFrom( 31 | foregroundColor: AppColors.darkGreen, 32 | ), 33 | ), 34 | tooltipTheme: 35 | const TooltipThemeData(textStyle: TextStyle(color: Colors.white)), 36 | tabBarTheme: const TabBarTheme( 37 | indicator: BoxDecoration( 38 | border: Border(), 39 | ), 40 | ), 41 | inputDecorationTheme: InputDecorationTheme( 42 | labelStyle: 43 | AppTextStyles.inputLabelText.copyWith(color: AppColors.grey), 44 | hintStyle: AppTextStyles.inputHintText.copyWith(color: AppColors.green), 45 | focusedBorder: defaultBorder, 46 | enabledBorder: defaultBorder, 47 | disabledBorder: defaultBorder, 48 | errorBorder: defaultBorder.copyWith( 49 | borderSide: const BorderSide( 50 | color: AppColors.error, 51 | ), 52 | ), 53 | focusedErrorBorder: defaultBorder.copyWith( 54 | borderSide: const BorderSide( 55 | color: AppColors.error, 56 | ), 57 | ), 58 | ), 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/common/utils/money_mask_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MoneyMaskedTextController extends TextEditingController { 4 | MoneyMaskedTextController( 5 | {double initialValue = 0.0, 6 | this.decimalSeparator = ',', 7 | this.thousandSeparator = '.', 8 | this.suffix = '', 9 | this.prefix = '', 10 | this.precision = 2}) { 11 | _validateConfig(); 12 | 13 | addListener(() { 14 | updateValue(numberValue); 15 | afterChange(text, numberValue); 16 | }); 17 | 18 | updateValue(initialValue); 19 | } 20 | 21 | final String decimalSeparator; 22 | final String thousandSeparator; 23 | final String suffix; 24 | final String prefix; 25 | final int precision; 26 | 27 | Function afterChange = (String maskedValue, double rawValue) {}; 28 | 29 | double _lastValue = 0.0; 30 | 31 | void updateValue(double value) { 32 | double valueToUse = value; 33 | 34 | if (value.toStringAsFixed(0).length > 12) { 35 | valueToUse = _lastValue; 36 | } else { 37 | _lastValue = value; 38 | } 39 | 40 | String masked = _applyMask(valueToUse); 41 | 42 | if (suffix.isNotEmpty) { 43 | masked += suffix; 44 | } 45 | 46 | if (prefix.isNotEmpty) { 47 | masked = prefix + masked; 48 | } 49 | 50 | if (masked != text) { 51 | text = masked; 52 | 53 | var cursorPosition = super.text.length - suffix.length; 54 | selection = 55 | TextSelection.fromPosition(TextPosition(offset: cursorPosition)); 56 | } 57 | } 58 | 59 | double get numberValue { 60 | List parts = _getOnlyNumbers(text).split('').toList(growable: true); 61 | 62 | parts.insert(parts.length - precision, '.'); 63 | 64 | return double.parse(parts.join()); 65 | } 66 | 67 | _validateConfig() { 68 | bool rightSymbolHasNumbers = _getOnlyNumbers(suffix).isNotEmpty; 69 | 70 | if (rightSymbolHasNumbers) { 71 | throw ArgumentError("rightSymbol must not have numbers."); 72 | } 73 | } 74 | 75 | String _getOnlyNumbers(String text) { 76 | String cleanedText = text; 77 | 78 | var onlyNumbersRegex = RegExp(r'[^\d]'); 79 | 80 | cleanedText = cleanedText.replaceAll(onlyNumbersRegex, ''); 81 | 82 | return cleanedText; 83 | } 84 | 85 | String _applyMask(double value) { 86 | List textRepresentation = value 87 | .toStringAsFixed(precision) 88 | .replaceAll('.', '') 89 | .split('') 90 | .reversed 91 | .toList(growable: true); 92 | 93 | textRepresentation.insert(precision, decimalSeparator); 94 | 95 | for (var i = precision + 4; true; i = i + 4) { 96 | if (textRepresentation.length > i) { 97 | textRepresentation.insert(i, thousandSeparator); 98 | } else { 99 | break; 100 | } 101 | } 102 | 103 | return textRepresentation.reversed.join(''); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/common/utils/uppercase_text_formatter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | ///Classe auxiliar que sobrecarrega a atuação do TextInpuFormatter 4 | ///para formatar o texto digitado no campo para maiúsculo 5 | /// 6 | ///Fonte: https://stackoverflow.com/a/49239762 7 | class UpperCaseTextInputFormatter extends TextInputFormatter { 8 | @override 9 | TextEditingValue formatEditUpdate( 10 | TextEditingValue oldValue, 11 | TextEditingValue newValue, 12 | ) { 13 | return TextEditingValue( 14 | text: newValue.text.toUpperCase(), 15 | selection: newValue.selection, 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/common/utils/utils.dart: -------------------------------------------------------------------------------- 1 | export 'money_mask_controller.dart'; 2 | export 'uppercase_text_formatter.dart'; 3 | export 'validator.dart'; 4 | -------------------------------------------------------------------------------- /lib/common/utils/validator.dart: -------------------------------------------------------------------------------- 1 | class Validator { 2 | Validator._(); 3 | 4 | static String? validateName(String? value) { 5 | final condition = RegExp(r"((\ *)[\wáéíóúñ]+(\ *)+)+"); 6 | if (value != null && value.isEmpty) { 7 | return "Esse campo não pode ser vazio."; 8 | } 9 | if (value != null && !condition.hasMatch(value)) { 10 | return "Nome inválido. Digite um nome válido."; 11 | } 12 | return null; 13 | } 14 | 15 | static String? validateEmail(String? value) { 16 | final condition = RegExp( 17 | r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"); 18 | if (value != null && value.isEmpty) { 19 | return "Esse campo não pode ser vazio."; 20 | } 21 | if (value != null && !condition.hasMatch(value)) { 22 | return "Email inválido. Digite um email válido."; 23 | } 24 | return null; 25 | } 26 | 27 | static String? validatePassword(String? value) { 28 | final condition = 29 | RegExp(r"^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$"); 30 | if (value != null && value.isEmpty) { 31 | return "Esse campo não pode ser vazio."; 32 | } 33 | if (value != null && !condition.hasMatch(value)) { 34 | return "Senha inválida. Digite uma senha válida."; 35 | } 36 | return null; 37 | } 38 | 39 | static String? validateConfirmPassword( 40 | String? passwordValue, 41 | String? confirmPasswordValue, 42 | ) { 43 | if (passwordValue != confirmPasswordValue) { 44 | return "As senhas são diferentes. Por favor, corrija para continuar."; 45 | } 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/common/widgets/app_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../constants/constants.dart'; 4 | import '../extensions/extensions.dart'; 5 | import 'widgets.dart'; 6 | 7 | class AppHeader extends StatefulWidget { 8 | const AppHeader({ 9 | super.key, 10 | this.title, 11 | this.suffixOption = false, 12 | this.preffixOption = false, 13 | this.onPressed, 14 | }) : _withBackground = true; 15 | 16 | const AppHeader.noBackground({ 17 | super.key, 18 | this.title, 19 | this.suffixOption = false, 20 | this.preffixOption = false, 21 | this.onPressed, 22 | }) : _withBackground = false; 23 | 24 | final String? title; 25 | final bool suffixOption; 26 | final bool preffixOption; 27 | final VoidCallback? onPressed; 28 | final bool _withBackground; 29 | 30 | @override 31 | State createState() => _AppHeaderState(); 32 | } 33 | 34 | class _AppHeaderState extends State { 35 | Widget get _child => widget.title != null 36 | ? Text( 37 | textAlign: TextAlign.center, 38 | widget.title!, 39 | style: AppTextStyles.mediumText18.apply( 40 | color: 41 | widget._withBackground ? AppColors.white : AppColors.blackGrey, 42 | ), 43 | ) 44 | : const Row( 45 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 46 | children: [ 47 | GreetingsWidget(), 48 | // TODO: implement notifications widget and page 49 | // NotificationWidget(), 50 | ], 51 | ); 52 | 53 | @override 54 | Widget build(BuildContext context) { 55 | return Stack( 56 | children: [ 57 | if (widget._withBackground) 58 | Positioned( 59 | left: 0, 60 | right: 0, 61 | child: Container( 62 | decoration: const BoxDecoration( 63 | gradient: LinearGradient( 64 | begin: Alignment.topCenter, 65 | end: Alignment.bottomCenter, 66 | colors: AppColors.greenGradient, 67 | ), 68 | borderRadius: BorderRadius.only( 69 | bottomLeft: Radius.elliptical(500, 30), 70 | bottomRight: Radius.elliptical(500, 30), 71 | ), 72 | ), 73 | height: 287.h, 74 | ), 75 | ), 76 | Positioned( 77 | left: 24.0.w, 78 | right: 24.0.w, 79 | top: 74.h, 80 | child: _child, 81 | ), 82 | if (widget.preffixOption) 83 | Positioned( 84 | left: 8.0.w, 85 | top: 56.h, 86 | child: GestureDetector( 87 | onTap: widget.onPressed ?? () => Navigator.pop(context), 88 | child: const Padding( 89 | padding: EdgeInsets.all(16.0), 90 | child: Icon( 91 | Icons.arrow_back_ios, 92 | color: AppColors.white, 93 | ), 94 | ), 95 | ), 96 | ), 97 | if (widget.suffixOption) 98 | Positioned( 99 | right: 8.0.w, 100 | top: 56.0.h, 101 | child: const Padding( 102 | padding: EdgeInsets.all(16.0), 103 | child: Icon( 104 | Icons.more_horiz, 105 | color: AppColors.white, 106 | ), 107 | ), 108 | ), 109 | ], 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/common/widgets/base_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../constants/app_colors.dart'; 4 | 5 | class BasePage extends StatelessWidget { 6 | const BasePage({ 7 | super.key, 8 | required this.child, 9 | }); 10 | final Widget child; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Container( 15 | decoration: const BoxDecoration( 16 | color: AppColors.white, 17 | borderRadius: BorderRadius.horizontal( 18 | left: Radius.circular( 19 | 30.0, 20 | ), 21 | right: Radius.circular( 22 | 30.0, 23 | ), 24 | ), 25 | ), 26 | child: child, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/common/widgets/custom_bottom_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../constants/app_colors.dart'; 4 | import '../extensions/page_controller_ext.dart'; 5 | 6 | class CustomBottomAppBar extends StatefulWidget { 7 | final PageController controller; 8 | final Color? selectedItemColor; 9 | final List children; 10 | const CustomBottomAppBar({ 11 | Key? key, 12 | this.selectedItemColor, 13 | required this.children, 14 | required this.controller, 15 | }) : assert(children.length == 5, 'children.length must be 5'), 16 | super(key: key); 17 | 18 | @override 19 | State createState() => _CustomBottomAppBarState(); 20 | } 21 | 22 | class _CustomBottomAppBarState extends State { 23 | @override 24 | void initState() { 25 | super.initState(); 26 | 27 | widget.controller.addListener(_handlePageChange); 28 | } 29 | 30 | @override 31 | void dispose() { 32 | widget.controller.dispose(); 33 | super.dispose(); 34 | } 35 | 36 | void _handlePageChange() { 37 | setState(() {}); 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return BottomAppBar( 43 | shape: const CircularNotchedRectangle(), 44 | child: Row( 45 | mainAxisAlignment: MainAxisAlignment.spaceAround, 46 | children: widget.children.map( 47 | (item) { 48 | bool currentItem; 49 | 50 | currentItem = widget.children.indexOf(item) == 51 | widget.controller.selectedBottomAppBarItemIndex; 52 | return Builder( 53 | builder: (context) { 54 | return Expanded( 55 | key: item.key, 56 | child: InkWell( 57 | onTap: item.onPressed, 58 | onTapUp: (_) { 59 | widget.controller.setBottomAppBarItemIndex = 60 | widget.children.indexOf(item); 61 | }, 62 | child: Padding( 63 | padding: const EdgeInsets.symmetric(vertical: 12.0), 64 | child: Icon( 65 | currentItem ? item.primaryIcon : item.secondaryIcon, 66 | color: currentItem 67 | ? widget.selectedItemColor 68 | : AppColors.lightGrey, 69 | ), 70 | ), 71 | ), 72 | ); 73 | }, 74 | ); 75 | }, 76 | ).toList(), 77 | ), 78 | ); 79 | } 80 | } 81 | 82 | class CustomBottomAppBarItem { 83 | final Key? key; 84 | final String? label; 85 | final IconData? primaryIcon; 86 | final IconData? secondaryIcon; 87 | final VoidCallback? onPressed; 88 | 89 | CustomBottomAppBarItem({ 90 | this.key, 91 | this.label, 92 | this.primaryIcon, 93 | this.secondaryIcon, 94 | this.onPressed, 95 | }); 96 | 97 | CustomBottomAppBarItem.empty({ 98 | this.key, 99 | this.label, 100 | this.primaryIcon, 101 | this.secondaryIcon, 102 | this.onPressed, 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /lib/common/widgets/custom_bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../constants/app_colors.dart'; 4 | import '../constants/app_text_styles.dart'; 5 | import 'primary_button.dart'; 6 | 7 | mixin CustomModalSheetMixin on State { 8 | Future showCustomModalBottomSheet({ 9 | required BuildContext context, 10 | required String content, 11 | String? buttonText, 12 | VoidCallback? onPressed, 13 | List? actions, 14 | bool isDismissible = true, 15 | }) { 16 | assert(buttonText != null || actions != null); 17 | 18 | return showModalBottomSheet( 19 | isDismissible: isDismissible, 20 | context: context, 21 | shape: const RoundedRectangleBorder( 22 | borderRadius: BorderRadius.only( 23 | topLeft: Radius.circular(38.0), 24 | topRight: Radius.circular(38.0), 25 | ), 26 | ), 27 | builder: (BuildContext context) { 28 | return PopScope( 29 | canPop: isDismissible, 30 | child: Container( 31 | decoration: const BoxDecoration( 32 | color: AppColors.white, 33 | borderRadius: BorderRadius.only( 34 | topLeft: Radius.circular(38.0), 35 | topRight: Radius.circular(38.0), 36 | ), 37 | ), 38 | height: 200, 39 | child: Column( 40 | mainAxisAlignment: MainAxisAlignment.center, 41 | mainAxisSize: MainAxisSize.min, 42 | children: [ 43 | Padding( 44 | padding: const EdgeInsets.symmetric( 45 | vertical: 16.0, 46 | horizontal: 32.0, 47 | ), 48 | child: Text( 49 | content, 50 | style: AppTextStyles.mediumText20.copyWith( 51 | color: AppColors.greenOne, 52 | ), 53 | ), 54 | ), 55 | Padding( 56 | padding: const EdgeInsets.symmetric( 57 | vertical: 8.0, 58 | horizontal: 32.0, 59 | ), 60 | child: actions != null 61 | ? Row( 62 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 63 | children: actions, 64 | ) 65 | : PrimaryButton( 66 | text: buttonText!, 67 | onPressed: onPressed ?? () => Navigator.pop(context), 68 | ), 69 | ), 70 | ], 71 | ), 72 | ), 73 | ); 74 | }, 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/common/widgets/custom_circular_progress_indicator.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../constants/app_colors.dart'; 5 | 6 | class CustomCircularProgressIndicator extends StatelessWidget { 7 | final Color? color; 8 | const CustomCircularProgressIndicator({ 9 | Key? key, 10 | this.color, 11 | }) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Center( 16 | child: CircularProgressIndicator( 17 | color: color ?? AppColors.iceWhite, 18 | ), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/common/widgets/custom_snackbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../constants/app_colors.dart'; 4 | import '../constants/app_text_styles.dart'; 5 | 6 | enum SnackBarType { success, warning, error, general } 7 | 8 | mixin CustomSnackBar on State { 9 | void showCustomSnackBar({ 10 | required BuildContext context, 11 | required String text, 12 | SnackBarType type = SnackBarType.general, 13 | }) { 14 | Color setColor() { 15 | switch (type) { 16 | case SnackBarType.error: 17 | return AppColors.error; 18 | case SnackBarType.success: 19 | return AppColors.green; 20 | case SnackBarType.warning: 21 | return AppColors.notification; 22 | case SnackBarType.general: 23 | return AppColors.grey; 24 | } 25 | } 26 | 27 | ScaffoldMessenger.of(context).removeCurrentSnackBar(); 28 | 29 | ScaffoldMessenger.of(context).showSnackBar( 30 | SnackBar( 31 | content: Text( 32 | text, 33 | style: AppTextStyles.smallText.apply( 34 | color: AppColors.iceWhite, 35 | ), 36 | ), 37 | backgroundColor: setColor(), 38 | closeIconColor: AppColors.iceWhite, 39 | behavior: SnackBarBehavior.floating, 40 | ), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/common/widgets/greetings_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/features/home/home.dart'; 2 | import 'package:financy_app/locator.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | import '../constants/constants.dart'; 6 | import '../extensions/extensions.dart'; 7 | 8 | class GreetingsWidget extends StatelessWidget { 9 | const GreetingsWidget({ 10 | super.key, 11 | }); 12 | 13 | String get _greeting { 14 | final hour = DateTime.now().hour; 15 | 16 | if (hour < 12) { 17 | return 'Good morning,'; 18 | } else if (hour < 18) { 19 | return 'Good afternoon,'; 20 | } else { 21 | return 'Good evening,'; 22 | } 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | double textScaleFactor = MediaQuery.sizeOf(context).width < 360 ? 0.7 : 1.0; 28 | 29 | return Column( 30 | crossAxisAlignment: CrossAxisAlignment.start, 31 | children: [ 32 | Text( 33 | _greeting, 34 | textScaler: TextScaler.linear(textScaleFactor), 35 | style: AppTextStyles.smallText.apply(color: AppColors.white), 36 | ), 37 | Text( 38 | (locator.get().userData.name ?? '') 39 | .capitalize() 40 | .firstWord, 41 | textScaler: TextScaler.linear(textScaleFactor), 42 | style: AppTextStyles.mediumText20.apply(color: AppColors.white), 43 | ), 44 | ], 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/common/widgets/multi_text_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MultiTextButton extends StatelessWidget { 4 | final List children; 5 | final VoidCallback onPressed; 6 | 7 | const MultiTextButton({ 8 | Key? key, 9 | required this.children, 10 | required this.onPressed, 11 | }) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return TextButton( 16 | onPressed: onPressed, 17 | child: Row( 18 | mainAxisSize: MainAxisSize.min, 19 | mainAxisAlignment: MainAxisAlignment.center, 20 | children: children, 21 | ), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/common/widgets/notification_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../constants/app_colors.dart'; 4 | import '../extensions/sizes.dart'; 5 | 6 | class NotificationWidget extends StatelessWidget { 7 | const NotificationWidget({ 8 | super.key, 9 | }); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Container( 14 | padding: EdgeInsets.symmetric( 15 | vertical: 8.h, 16 | horizontal: 8.w, 17 | ), 18 | decoration: BoxDecoration( 19 | borderRadius: const BorderRadius.all(Radius.circular(4.0)), 20 | color: AppColors.white.withOpacity(0.06), 21 | ), 22 | child: Stack( 23 | alignment: const AlignmentDirectional(0.5, -0.5), 24 | children: [ 25 | const Icon( 26 | Icons.notifications_none_outlined, 27 | color: AppColors.white, 28 | ), 29 | Container( 30 | width: 8.w, 31 | height: 8.w, 32 | decoration: BoxDecoration( 33 | color: AppColors.notification, 34 | borderRadius: BorderRadius.circular( 35 | 4.0, 36 | ), 37 | ), 38 | ), 39 | ], 40 | ), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/common/widgets/password_form_field.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | import 'dart:developer'; 3 | 4 | import 'package:financy_app/common/constants/app_colors.dart'; 5 | import 'package:financy_app/common/widgets/custom_text_form_field.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | class PasswordFormField extends StatefulWidget { 9 | final TextEditingController? controller; 10 | final EdgeInsetsGeometry? padding; 11 | final String? hintText; 12 | final String? labelText; 13 | final FormFieldValidator? validator; 14 | final String? helperText; 15 | final FocusNode? focusNode; 16 | final VoidCallback? onTap; 17 | final ValueSetter? onTapOutside; 18 | final VoidCallback? onEditingComplete; 19 | 20 | const PasswordFormField({ 21 | Key? key, 22 | this.controller, 23 | this.padding, 24 | this.hintText, 25 | this.labelText, 26 | this.validator, 27 | this.helperText, 28 | this.focusNode, 29 | this.onTap, 30 | this.onTapOutside, 31 | this.onEditingComplete, 32 | }) : super(key: key); 33 | 34 | @override 35 | State createState() => _PasswordFormFieldState(); 36 | } 37 | 38 | class _PasswordFormFieldState extends State { 39 | bool isHidden = true; 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return CustomTextFormField( 44 | onTap: widget.onTap, 45 | onEditingComplete: widget.onEditingComplete ?? 46 | () { 47 | FocusScope.of(context).nextFocus(); 48 | }, 49 | focusNode: widget.focusNode, 50 | onTapOutside: widget.onTapOutside ?? 51 | (_) { 52 | if (FocusScope.of(context).hasFocus) { 53 | FocusScope.of(context).unfocus(); 54 | } 55 | }, 56 | helperText: widget.helperText, 57 | validator: widget.validator, 58 | obscureText: isHidden, 59 | controller: widget.controller, 60 | padding: widget.padding, 61 | hintText: widget.hintText, 62 | labelText: widget.labelText, 63 | suffixIcon: InkWell( 64 | borderRadius: BorderRadius.circular(23.0), 65 | onTap: () { 66 | log("pressed"); 67 | setState(() { 68 | isHidden = !isHidden; 69 | }); 70 | }, 71 | child: Icon( 72 | isHidden ? Icons.visibility : Icons.visibility_off, 73 | color: AppColors.greenTwo, 74 | ), 75 | ), 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/common/widgets/primary_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../constants/app_colors.dart'; 4 | import '../constants/app_text_styles.dart'; 5 | 6 | class PrimaryButton extends StatelessWidget { 7 | final VoidCallback? onPressed; 8 | final String text; 9 | 10 | const PrimaryButton({ 11 | Key? key, 12 | this.onPressed, 13 | required this.text, 14 | }) : super(key: key); 15 | 16 | final BorderRadius _borderRadius = 17 | const BorderRadius.all(Radius.circular(24.0)); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Material( 22 | color: Colors.transparent, 23 | child: Ink( 24 | height: 48.0, 25 | decoration: BoxDecoration( 26 | borderRadius: _borderRadius, 27 | gradient: LinearGradient( 28 | begin: Alignment.topCenter, 29 | end: Alignment.bottomCenter, 30 | colors: onPressed != null 31 | ? AppColors.greenGradient 32 | : AppColors.greyGradient, 33 | ), 34 | ), 35 | child: InkWell( 36 | borderRadius: _borderRadius, 37 | onTap: onPressed, 38 | child: Align( 39 | child: Text( 40 | text, 41 | style: AppTextStyles.mediumText18.copyWith( 42 | color: AppColors.white, 43 | ), 44 | ), 45 | ), 46 | ), 47 | ), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/common/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'app_header.dart'; 2 | export 'base_page.dart'; 3 | export 'custom_bottom_app_bar.dart'; 4 | export 'custom_bottom_sheet.dart'; 5 | export 'custom_circular_progress_indicator.dart'; 6 | export 'custom_snackbar.dart'; 7 | export 'custom_text_form_field.dart'; 8 | export 'greetings_widget.dart'; 9 | export 'multi_text_button.dart'; 10 | export 'notification_widget.dart'; 11 | export 'password_form_field.dart'; 12 | export 'primary_button.dart'; 13 | export 'transaction_listview.dart'; 14 | -------------------------------------------------------------------------------- /lib/features/forgot_password/check_your_email_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/common/constants/constants.dart'; 2 | import 'package:financy_app/common/widgets/primary_button.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class CheckYourEmailPage extends StatelessWidget { 6 | const CheckYourEmailPage({super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Scaffold( 11 | body: Padding( 12 | padding: const EdgeInsets.symmetric( 13 | horizontal: 24.0, 14 | vertical: 16.0, 15 | ), 16 | child: ListView( 17 | children: [ 18 | Text( 19 | 'Reset Your\nPassword', 20 | textAlign: TextAlign.center, 21 | style: AppTextStyles.mediumText36.copyWith( 22 | color: AppColors.greenOne, 23 | ), 24 | ), 25 | Image.asset('assets/images/check_your_email_image.png'), 26 | Padding( 27 | padding: const EdgeInsets.symmetric(vertical: 16.0), 28 | child: Text( 29 | "All set! Follow the instructions on your email to reset your password. Don't forget to check the spam box!", 30 | textAlign: TextAlign.center, 31 | style: AppTextStyles.mediumText16w500.copyWith( 32 | color: AppColors.darkGrey, 33 | ), 34 | ), 35 | ), 36 | PrimaryButton( 37 | text: 'Login', 38 | onPressed: () => Navigator.pushNamedAndRemoveUntil( 39 | context, 40 | NamedRoute.signIn, 41 | ModalRoute.withName(NamedRoute.initial), 42 | )), 43 | ], 44 | ), 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/features/forgot_password/forgot_password_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/services/auth_service/auth_service.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | import 'forgot_password_state.dart'; 5 | 6 | class ForgotPasswordController extends ChangeNotifier { 7 | ForgotPasswordController({ 8 | required AuthService authService, 9 | }) : _authService = authService; 10 | 11 | final AuthService _authService; 12 | 13 | ForgotPasswordState _state = ForgotPasswordStateInitial(); 14 | 15 | ForgotPasswordState get state => _state; 16 | 17 | void _changeState(ForgotPasswordState newState) { 18 | _state = newState; 19 | notifyListeners(); 20 | } 21 | 22 | Future forgotPassword(String email) async { 23 | _changeState(ForgotPasswordStateLoading()); 24 | final result = await _authService.forgotPassword(email); 25 | result.fold( 26 | (error) => _changeState(ForgotPasswordStateError(error.message)), 27 | (data) => _changeState(ForgotPasswordStateSuccess()), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/features/forgot_password/forgot_password_state.dart: -------------------------------------------------------------------------------- 1 | abstract class ForgotPasswordState {} 2 | 3 | class ForgotPasswordStateInitial extends ForgotPasswordState {} 4 | 5 | class ForgotPasswordStateLoading extends ForgotPasswordState {} 6 | 7 | class ForgotPasswordStateSuccess extends ForgotPasswordState {} 8 | 9 | class ForgotPasswordStateError extends ForgotPasswordState { 10 | final String message; 11 | 12 | ForgotPasswordStateError(this.message); 13 | } 14 | -------------------------------------------------------------------------------- /lib/features/home/home.dart: -------------------------------------------------------------------------------- 1 | export 'home_controller.dart'; 2 | export 'home_page.dart'; 3 | export 'home_page_view.dart'; 4 | export 'home_state.dart'; 5 | -------------------------------------------------------------------------------- /lib/features/home/home_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../common/models/models.dart'; 4 | import '../../repositories/repositories.dart'; 5 | import '../../services/services.dart'; 6 | import 'home_state.dart'; 7 | 8 | class HomeController extends ChangeNotifier { 9 | HomeController({ 10 | required TransactionRepository transactionRepository, 11 | required UserDataService userDataService, 12 | }) : _userDataService = userDataService, 13 | _transactionRepository = transactionRepository; 14 | 15 | final TransactionRepository _transactionRepository; 16 | final UserDataService _userDataService; 17 | 18 | HomeState _state = HomeStateInitial(); 19 | 20 | HomeState get state => _state; 21 | 22 | UserModel get userData => _userDataService.userData; 23 | 24 | late PageController _pageController; 25 | PageController get pageController => _pageController; 26 | 27 | List _transactions = []; 28 | List get transactions => _transactions; 29 | 30 | set setPageController(PageController newPageController) { 31 | _pageController = newPageController; 32 | } 33 | 34 | void _changeState(HomeState newState) { 35 | _state = newState; 36 | notifyListeners(); 37 | } 38 | 39 | Future getLatestTransactions() async { 40 | _changeState(HomeStateLoading()); 41 | 42 | final result = await _transactionRepository.getLatestTransactions(); 43 | 44 | result.fold( 45 | (error) => _changeState(HomeStateError(message: error.message)), 46 | (data) { 47 | _transactions = data; 48 | _transactions.sort((a, b) => b.createdAt.compareTo(a.createdAt)); 49 | 50 | _changeState(HomeStateSuccess()); 51 | }, 52 | ); 53 | } 54 | 55 | Future getUserData() async { 56 | _changeState(HomeStateLoading()); 57 | final result = await _userDataService.getUserData(); 58 | 59 | result.fold( 60 | (error) => _changeState(HomeStateError(message: error.message)), 61 | (data) => _changeState(HomeStateSuccess()), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/features/home/home_state.dart: -------------------------------------------------------------------------------- 1 | abstract class HomeState {} 2 | 3 | class HomeStateInitial extends HomeState {} 4 | 5 | class HomeStateLoading extends HomeState {} 6 | 7 | class HomeStateSuccess extends HomeState {} 8 | 9 | class HomeStateError extends HomeState { 10 | HomeStateError({required this.message}); 11 | 12 | final String message; 13 | } 14 | -------------------------------------------------------------------------------- /lib/features/onboarding/onboarding.dart: -------------------------------------------------------------------------------- 1 | export 'onboarding_page.dart'; 2 | -------------------------------------------------------------------------------- /lib/features/onboarding/onboarding_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../common/constants/constants.dart'; 4 | import '../../common/widgets/widgets.dart'; 5 | 6 | class OnboardingPage extends StatelessWidget { 7 | const OnboardingPage({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | backgroundColor: AppColors.iceWhite, 13 | body: Column( 14 | children: [ 15 | const SizedBox(height: 48.0), 16 | Expanded( 17 | child: Image.asset( 18 | 'assets/images/onboarding_image.png', 19 | ), 20 | ), 21 | Text( 22 | 'Spend Smarter', 23 | textAlign: TextAlign.center, 24 | style: AppTextStyles.mediumText36.copyWith( 25 | color: AppColors.greenOne, 26 | ), 27 | ), 28 | Text( 29 | 'Save More', 30 | textAlign: TextAlign.center, 31 | style: AppTextStyles.mediumText36.copyWith( 32 | color: AppColors.greenOne, 33 | ), 34 | ), 35 | Padding( 36 | padding: const EdgeInsets.only( 37 | left: 32.0, 38 | right: 32.0, 39 | top: 16.0, 40 | bottom: 4.0, 41 | ), 42 | child: PrimaryButton( 43 | key: Keys.onboardingGetStartedButton, 44 | text: 'Get Started', 45 | onPressed: () { 46 | Navigator.pushNamed( 47 | context, 48 | NamedRoute.signUp, 49 | ); 50 | }, 51 | ), 52 | ), 53 | MultiTextButton( 54 | key: Keys.onboardingAlreadyHaveAccountButton, 55 | onPressed: () => Navigator.pushNamed(context, NamedRoute.signIn), 56 | children: [ 57 | Text( 58 | 'Already have account? ', 59 | style: AppTextStyles.smallText.copyWith( 60 | color: AppColors.grey, 61 | ), 62 | ), 63 | Text( 64 | 'Sign In ', 65 | style: AppTextStyles.smallText.copyWith( 66 | color: AppColors.greenOne, 67 | ), 68 | ), 69 | ], 70 | ), 71 | const SizedBox(height: 24.0), 72 | ], 73 | ), 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/features/profile/profile.dart: -------------------------------------------------------------------------------- 1 | export 'profile_controller.dart'; 2 | export 'profile_page.dart'; 3 | export 'profile_state.dart'; 4 | -------------------------------------------------------------------------------- /lib/features/profile/profile_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | import '../../common/models/models.dart'; 4 | import '../../services/services.dart'; 5 | import 'profile_state.dart'; 6 | 7 | class ProfileController extends ChangeNotifier { 8 | ProfileController({required UserDataService userDataService}) 9 | : _userDataService = userDataService; 10 | 11 | final UserDataService _userDataService; 12 | 13 | ProfileState _state = ProfileStateInitial(); 14 | 15 | ProfileState get state => _state; 16 | 17 | UserModel get userData => _userDataService.userData; 18 | 19 | bool get canSave => enabledButton && state is! ProfileStateLoading; 20 | 21 | void _changeState(ProfileState newState) { 22 | _state = newState; 23 | notifyListeners(); 24 | } 25 | 26 | bool _reauthRequired = false; 27 | bool get reauthRequired => _reauthRequired; 28 | 29 | bool _showUpdatedNameMessage = false; 30 | bool get showNameUpdateMessage => 31 | _showUpdatedNameMessage && state is ProfileStateSuccess; 32 | 33 | bool _showUpdatedPasswordMessage = false; 34 | bool get showPasswordUpdateMessage => 35 | _showUpdatedPasswordMessage && state is ProfileStateSuccess; 36 | 37 | bool _showChangeName = false; 38 | bool get showChangeName => _showChangeName; 39 | void onChangeNameTapped() { 40 | _showChangeName = !showChangeName; 41 | _changeState(ProfileStateInitial()); 42 | } 43 | 44 | bool _showChangePassword = false; 45 | bool get showChangePassword => _showChangePassword; 46 | void onChangePasswordTapped() { 47 | _showChangePassword = !showChangePassword; 48 | _changeState(ProfileStateInitial()); 49 | } 50 | 51 | bool _enabledButton = false; 52 | bool get enabledButton => _enabledButton; 53 | void toggleButtonTap(bool value) { 54 | _enabledButton = value; 55 | _changeState(ProfileStateInitial()); 56 | } 57 | 58 | Future getUserData() async { 59 | final result = await _userDataService.getUserData(); 60 | 61 | result.fold( 62 | (error) => _changeState(ProfileStateError(message: error.message)), 63 | (data) => _changeState(ProfileStateSuccess(user: data)), 64 | ); 65 | } 66 | 67 | Future updateUserName(String newUserName) async { 68 | _changeState(ProfileStateLoading()); 69 | 70 | final result = await _userDataService.updateUserName(newUserName); 71 | result.fold( 72 | (error) => _changeState(ProfileStateError(message: error.message)), 73 | (_) { 74 | _showUpdatedNameMessage = true; 75 | _showUpdatedPasswordMessage = false; 76 | onChangeNameTapped(); 77 | toggleButtonTap(false); 78 | _changeState(ProfileStateSuccess()); 79 | }, 80 | ); 81 | } 82 | 83 | Future updateUserPassword(String newPassword) async { 84 | _changeState(ProfileStateLoading()); 85 | final result = await _userDataService.updatePassword(newPassword); 86 | result.fold( 87 | (error) { 88 | _reauthRequired = true; 89 | _changeState(ProfileStateError(message: error.message)); 90 | }, 91 | (_) { 92 | _showUpdatedPasswordMessage = true; 93 | _showUpdatedNameMessage = false; 94 | onChangePasswordTapped(); 95 | toggleButtonTap(false); 96 | _changeState(ProfileStateSuccess()); 97 | }, 98 | ); 99 | } 100 | 101 | Future deleteAccount() async { 102 | _changeState(ProfileStateLoading()); 103 | final result = await _userDataService.deleteAccount(); 104 | result.fold( 105 | (error) => _changeState(ProfileStateError(message: error.message)), 106 | (_) => _changeState(ProfileStateSuccess()), 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/features/profile/profile_state.dart: -------------------------------------------------------------------------------- 1 | import '../../common/models/models.dart'; 2 | 3 | abstract class ProfileState {} 4 | 5 | class ProfileStateInitial extends ProfileState {} 6 | 7 | class ProfileStateLoading extends ProfileState {} 8 | 9 | class ProfileStateSuccess extends ProfileState { 10 | ProfileStateSuccess({this.user}); 11 | 12 | final UserModel? user; 13 | } 14 | 15 | class ProfileStateError extends ProfileState { 16 | ProfileStateError({required this.message}); 17 | 18 | final String message; 19 | } 20 | -------------------------------------------------------------------------------- /lib/features/profile/widgets/profile_change_name_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../common/constants/constants.dart'; 4 | import '../../../common/utils/utils.dart'; 5 | import '../../../common/widgets/widgets.dart'; 6 | import '../profile.dart'; 7 | 8 | class ProfileChangeNameWidget extends StatefulWidget { 9 | const ProfileChangeNameWidget({ 10 | super.key, 11 | required ProfileController profileController, 12 | }) : _profileController = profileController; 13 | 14 | final ProfileController _profileController; 15 | 16 | @override 17 | State createState() => 18 | _ProfileChangeNameWidgetState(); 19 | } 20 | 21 | class _ProfileChangeNameWidgetState extends State 22 | with CustomSnackBar { 23 | final _textEditingController = TextEditingController(); 24 | final _focusNode = FocusNode(); 25 | final _formKey = GlobalKey(); 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | 31 | _textEditingController.addListener(handleNameChange); 32 | } 33 | 34 | @override 35 | void dispose() { 36 | _textEditingController.dispose(); 37 | 38 | super.dispose(); 39 | } 40 | 41 | void handleNameChange() { 42 | if (_formKey.currentState != null && _focusNode.hasFocus) { 43 | widget._profileController 44 | .toggleButtonTap(_formKey.currentState?.validate() ?? false); 45 | } 46 | } 47 | 48 | Future onNewNameSavePressed() async { 49 | if (_focusNode.hasFocus) _focusNode.unfocus(); 50 | 51 | await widget._profileController.updateUserName(_textEditingController.text); 52 | 53 | _textEditingController.clear(); 54 | } 55 | 56 | @override 57 | Widget build(BuildContext context) { 58 | return Column( 59 | key: UniqueKey(), 60 | children: [ 61 | Form( 62 | key: _formKey, 63 | child: CustomTextFormField( 64 | inputFormatters: [UpperCaseTextInputFormatter()], 65 | controller: _textEditingController, 66 | focusNode: _focusNode, 67 | labelText: 'New name', 68 | onTapOutside: (_) => _focusNode.unfocus(), 69 | validator: (_) => 70 | Validator.validateName(_textEditingController.text), 71 | onEditingComplete: 72 | widget._profileController.canSave ? onNewNameSavePressed : null, 73 | ), 74 | ), 75 | const SizedBox(height: 16.0), 76 | Row( 77 | mainAxisAlignment: MainAxisAlignment.spaceAround, 78 | children: [ 79 | Expanded( 80 | child: TextButton( 81 | onPressed: () { 82 | _textEditingController.clear(); 83 | widget._profileController.onChangeNameTapped(); 84 | widget._profileController.toggleButtonTap(false); 85 | }, 86 | child: Text( 87 | 'Cancel', 88 | style: AppTextStyles.mediumText16w500 89 | .apply(color: AppColors.green), 90 | ), 91 | ), 92 | ), 93 | const SizedBox(width: 16.0), 94 | Expanded( 95 | child: TextButton( 96 | onPressed: widget._profileController.canSave 97 | ? onNewNameSavePressed 98 | : null, 99 | child: Text( 100 | 'Save', 101 | style: AppTextStyles.mediumText16w500 102 | .apply(color: AppColors.green), 103 | ), 104 | ), 105 | ), 106 | ], 107 | ), 108 | ], 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/features/profile/widgets/profile_change_password_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../common/constants/constants.dart'; 4 | import '../../../common/utils/utils.dart'; 5 | import '../../../common/widgets/widgets.dart'; 6 | import '../profile.dart'; 7 | 8 | class ProfileChangePasswordWidget extends StatefulWidget { 9 | const ProfileChangePasswordWidget({ 10 | super.key, 11 | required ProfileController profileController, 12 | }) : _profileController = profileController; 13 | 14 | final ProfileController _profileController; 15 | 16 | @override 17 | State createState() => 18 | _ProfileChangePasswordWidgetState(); 19 | } 20 | 21 | class _ProfileChangePasswordWidgetState 22 | extends State with CustomSnackBar { 23 | final _textEditingController = TextEditingController(); 24 | final _focusNode = FocusNode(); 25 | final _formKey = GlobalKey(); 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | 31 | _textEditingController.addListener(handlePasswordChange); 32 | } 33 | 34 | @override 35 | void dispose() { 36 | _textEditingController.dispose(); 37 | _focusNode.dispose(); 38 | super.dispose(); 39 | } 40 | 41 | void handlePasswordChange() { 42 | if (_formKey.currentState != null && _focusNode.hasFocus) { 43 | widget._profileController 44 | .toggleButtonTap(_formKey.currentState?.validate() ?? false); 45 | } 46 | } 47 | 48 | Future onNewPasswordSavePressed() async { 49 | if (_focusNode.hasFocus) _focusNode.unfocus(); 50 | 51 | await widget._profileController 52 | .updateUserPassword(_textEditingController.text); 53 | } 54 | 55 | @override 56 | Widget build(BuildContext context) { 57 | return Column( 58 | key: UniqueKey(), 59 | children: [ 60 | Form( 61 | key: _formKey, 62 | child: PasswordFormField( 63 | controller: _textEditingController, 64 | focusNode: _focusNode, 65 | labelText: 'New password', 66 | onTapOutside: (_) => _focusNode.unfocus(), 67 | validator: (_) => 68 | Validator.validatePassword(_textEditingController.text), 69 | onEditingComplete: widget._profileController.canSave 70 | ? onNewPasswordSavePressed 71 | : null, 72 | ), 73 | ), 74 | const SizedBox(height: 16.0), 75 | Row( 76 | mainAxisAlignment: MainAxisAlignment.spaceAround, 77 | children: [ 78 | Expanded( 79 | child: TextButton( 80 | onPressed: () { 81 | widget._profileController.onChangePasswordTapped(); 82 | widget._profileController.toggleButtonTap(false); 83 | }, 84 | child: Text( 85 | 'Cancel', 86 | style: AppTextStyles.mediumText16w500 87 | .apply(color: AppColors.green), 88 | ), 89 | ), 90 | ), 91 | const SizedBox(width: 16.0), 92 | Expanded( 93 | child: TextButton( 94 | onPressed: widget._profileController.canSave 95 | ? onNewPasswordSavePressed 96 | : null, 97 | child: Text( 98 | 'Save', 99 | style: AppTextStyles.mediumText16w500 100 | .apply(color: AppColors.green), 101 | ), 102 | ), 103 | ), 104 | ], 105 | ), 106 | ], 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/features/sign_in/sign_in.dart: -------------------------------------------------------------------------------- 1 | export 'sign_in_controller.dart'; 2 | export 'sign_in_page.dart'; 3 | export 'sign_in_state.dart'; 4 | -------------------------------------------------------------------------------- /lib/features/sign_in/sign_in_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | import '../../services/services.dart'; 4 | import 'sign_in_state.dart'; 5 | 6 | class SignInController extends ChangeNotifier { 7 | SignInController({ 8 | required AuthService authService, 9 | required SecureStorageService secureStorageService, 10 | }) : _secureStorageService = secureStorageService, 11 | _authService = authService; 12 | 13 | final AuthService _authService; 14 | final SecureStorageService _secureStorageService; 15 | 16 | SignInState _state = SignInStateInitial(); 17 | 18 | SignInState get state => _state; 19 | 20 | void _changeState(SignInState newState) { 21 | _state = newState; 22 | notifyListeners(); 23 | } 24 | 25 | Future signIn({ 26 | required String email, 27 | required String password, 28 | }) async { 29 | _changeState(SignInStateLoading()); 30 | 31 | final result = await _authService.signIn( 32 | email: email, 33 | password: password, 34 | ); 35 | 36 | result.fold( 37 | (error) => _changeState(SignInStateError(error.message)), 38 | (data) async { 39 | await _secureStorageService.write( 40 | key: "CURRENT_USER", 41 | value: data.toJson(), 42 | ); 43 | 44 | result.fold( 45 | (error) => _changeState(SignInStateError(error.message)), 46 | (_) => _changeState(SignInStateSuccess()), 47 | ); 48 | }, 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/features/sign_in/sign_in_state.dart: -------------------------------------------------------------------------------- 1 | abstract class SignInState {} 2 | 3 | class SignInStateInitial extends SignInState {} 4 | 5 | class SignInStateLoading extends SignInState {} 6 | 7 | class SignInStateSuccess extends SignInState {} 8 | 9 | class SignInStateError extends SignInState { 10 | final String message; 11 | 12 | SignInStateError(this.message); 13 | } 14 | -------------------------------------------------------------------------------- /lib/features/sign_up/sign_up.dart: -------------------------------------------------------------------------------- 1 | export 'sign_up_controller.dart'; 2 | export 'sign_up_page.dart'; 3 | export 'sign_up_state.dart'; 4 | -------------------------------------------------------------------------------- /lib/features/sign_up/sign_up_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | import '../../services/services.dart'; 4 | import 'sign_up_state.dart'; 5 | 6 | class SignUpController extends ChangeNotifier { 7 | SignUpController({ 8 | required AuthService authService, 9 | required SecureStorageService secureStorageService, 10 | }) : _secureStorageService = secureStorageService, 11 | _authService = authService; 12 | 13 | final AuthService _authService; 14 | final SecureStorageService _secureStorageService; 15 | 16 | SignUpState _state = SignUpStateInitial(); 17 | 18 | SignUpState get state => _state; 19 | 20 | void _changeState(SignUpState newState) { 21 | _state = newState; 22 | notifyListeners(); 23 | } 24 | 25 | Future signUp({ 26 | required String name, 27 | required String email, 28 | required String password, 29 | }) async { 30 | _changeState(SignUpStateLoading()); 31 | 32 | final result = await _authService.signUp( 33 | name: name, 34 | email: email, 35 | password: password, 36 | ); 37 | 38 | result.fold( 39 | (error) => _changeState(SignUpStateError(error.message)), 40 | (data) async { 41 | await _secureStorageService.write( 42 | key: "CURRENT_USER", 43 | value: data.toJson(), 44 | ); 45 | 46 | _changeState(SignUpStateSuccess()); 47 | }, 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/features/sign_up/sign_up_state.dart: -------------------------------------------------------------------------------- 1 | abstract class SignUpState {} 2 | 3 | class SignUpStateInitial extends SignUpState {} 4 | 5 | class SignUpStateLoading extends SignUpState {} 6 | 7 | class SignUpStateSuccess extends SignUpState {} 8 | 9 | class SignUpStateError extends SignUpState { 10 | final String message; 11 | 12 | SignUpStateError(this.message); 13 | } 14 | -------------------------------------------------------------------------------- /lib/features/splash/splash.dart: -------------------------------------------------------------------------------- 1 | export 'splash_controller.dart'; 2 | export 'splash_page.dart'; 3 | export 'splash_state.dart'; 4 | -------------------------------------------------------------------------------- /lib/features/splash/splash_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | import '../../services/services.dart'; 4 | import 'splash_state.dart'; 5 | 6 | class SplashController extends ChangeNotifier { 7 | SplashController({ 8 | required this.secureStorageService, 9 | }); 10 | 11 | final SecureStorageService secureStorageService; 12 | 13 | SplashState _state = SplashStateInitial(); 14 | 15 | SplashState get state => _state; 16 | 17 | void _changeState(SplashState newState) { 18 | _state = newState; 19 | notifyListeners(); 20 | } 21 | 22 | Future isUserLogged() async { 23 | final result = await secureStorageService.readOne(key: "CURRENT_USER"); 24 | if (result != null) { 25 | _changeState(AuthenticatedUser()); 26 | } else { 27 | _changeState(UnauthenticatedUser()); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/features/splash/splash_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../common/constants/constants.dart'; 4 | import '../../common/extensions/extensions.dart'; 5 | import '../../common/widgets/widgets.dart'; 6 | import '../../locator.dart'; 7 | import '../../services/sync_service/sync_service.dart'; 8 | import 'splash_controller.dart'; 9 | import 'splash_state.dart'; 10 | 11 | class SplashPage extends StatefulWidget { 12 | const SplashPage({super.key}); 13 | 14 | @override 15 | State createState() => _SplashPageState(); 16 | } 17 | 18 | class _SplashPageState extends State with CustomModalSheetMixin { 19 | final _splashController = locator.get(); 20 | final _syncController = locator.get(); 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | 26 | WidgetsBinding.instance.addPostFrameCallback((_) => Sizes.init(context)); 27 | 28 | _splashController.isUserLogged(); 29 | _splashController.addListener(_handleSplashStateChange); 30 | _syncController.addListener(_handleSyncStateChange); 31 | } 32 | 33 | @override 34 | void dispose() { 35 | _splashController.dispose(); 36 | _syncController.dispose(); 37 | super.dispose(); 38 | } 39 | 40 | void _handleSplashStateChange() { 41 | if (_splashController.state is AuthenticatedUser) { 42 | _syncController.syncFromServer(); 43 | } else { 44 | Navigator.pushReplacementNamed( 45 | context, 46 | NamedRoute.initial, 47 | ); 48 | } 49 | } 50 | 51 | void _handleSyncStateChange() { 52 | final state = _syncController.state; 53 | 54 | switch (state.runtimeType) { 55 | case DownloadedDataFromServer: 56 | _syncController.syncToServer(); 57 | break; 58 | case UploadedDataToServer: 59 | Navigator.pushNamedAndRemoveUntil( 60 | context, 61 | NamedRoute.home, 62 | (route) => false, 63 | ); 64 | break; 65 | case SyncStateError: 66 | case UploadDataToServerError: 67 | case DownloadDataFromServerError: 68 | showCustomModalBottomSheet( 69 | context: context, 70 | content: (state as SyncStateError).message, 71 | buttonText: 'Go to login', 72 | isDismissible: false, 73 | onPressed: () => Navigator.pushNamedAndRemoveUntil( 74 | context, 75 | NamedRoute.initial, 76 | (route) => false, 77 | ), 78 | ); 79 | break; 80 | } 81 | } 82 | 83 | @override 84 | Widget build(BuildContext context) { 85 | return Scaffold( 86 | body: Container( 87 | alignment: Alignment.center, 88 | decoration: const BoxDecoration( 89 | gradient: LinearGradient( 90 | begin: Alignment.topCenter, 91 | end: Alignment.bottomCenter, 92 | colors: AppColors.greenGradient, 93 | ), 94 | ), 95 | child: Column( 96 | mainAxisAlignment: MainAxisAlignment.center, 97 | children: [ 98 | Text( 99 | 'financy', 100 | style: AppTextStyles.bigText50.copyWith(color: AppColors.white), 101 | ), 102 | Text( 103 | 'Syncing data...', 104 | style: AppTextStyles.smallText13.copyWith(color: AppColors.white), 105 | ), 106 | const SizedBox(height: 16.0), 107 | const CustomCircularProgressIndicator(), 108 | ], 109 | ), 110 | ), 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/features/splash/splash_state.dart: -------------------------------------------------------------------------------- 1 | abstract class SplashState {} 2 | 3 | class SplashStateInitial extends SplashState {} 4 | 5 | class AuthenticatedUser extends SplashState { 6 | AuthenticatedUser(); 7 | } 8 | 9 | class UnauthenticatedUser extends SplashState {} 10 | -------------------------------------------------------------------------------- /lib/features/stats/stats.dart: -------------------------------------------------------------------------------- 1 | export 'stats_page.dart'; 2 | -------------------------------------------------------------------------------- /lib/features/stats/stats_state.dart: -------------------------------------------------------------------------------- 1 | abstract class StatsState {} 2 | 3 | class StatsStateInitial extends StatsState {} 4 | 5 | class StatsStateLoading extends StatsState {} 6 | 7 | class StatsStateSuccess extends StatsState {} 8 | 9 | class StatsStateError extends StatsState {} 10 | -------------------------------------------------------------------------------- /lib/features/transactions/transactions.dart: -------------------------------------------------------------------------------- 1 | export 'transaction_page.dart'; 2 | -------------------------------------------------------------------------------- /lib/features/wallet/wallet.dart: -------------------------------------------------------------------------------- 1 | export 'wallet_controller.dart'; 2 | export 'wallet_page.dart'; 3 | export 'wallet_state.dart'; 4 | -------------------------------------------------------------------------------- /lib/features/wallet/wallet_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | import '../../common/models/transaction_model.dart'; 4 | import '../../repositories/transaction_repository.dart'; 5 | import 'wallet_state.dart'; 6 | 7 | class WalletController extends ChangeNotifier { 8 | WalletController({ 9 | required this.transactionRepository, 10 | }); 11 | 12 | final TransactionRepository transactionRepository; 13 | 14 | WalletState _state = WalletStateInitial(); 15 | 16 | WalletState get state => _state; 17 | 18 | DateTime _selectedDate = DateTime.now(); 19 | DateTime get selectedDate => _selectedDate; 20 | 21 | List _transactions = []; 22 | List get transactions => _transactions; 23 | 24 | void _changeState(WalletState newState) { 25 | _state = newState; 26 | notifyListeners(); 27 | } 28 | 29 | Future getAllTransactions() async { 30 | _changeState(WalletStateLoading()); 31 | 32 | final result = await transactionRepository.getLatestTransactions(); 33 | 34 | result.fold( 35 | (error) => _changeState(WalletStateError(message: error.message)), 36 | (data) { 37 | _transactions = data; 38 | 39 | _changeState(WalletStateSuccess()); 40 | }, 41 | ); 42 | } 43 | 44 | void changeSelectedDate(DateTime newDate) { 45 | _selectedDate = newDate; 46 | } 47 | 48 | Future getTransactionsByDateRange() async { 49 | _changeState(WalletStateLoading()); 50 | 51 | final result = await transactionRepository.getTransactionsByDateRange( 52 | startDate: _selectedDate.copyWith(day: 1), 53 | endDate: _selectedDate.copyWith( 54 | day: DateTime(_selectedDate.year, _selectedDate.month + 1, 0).day, 55 | ), 56 | ); 57 | 58 | result.fold( 59 | (error) => _changeState(WalletStateError(message: error.message)), 60 | (data) { 61 | _transactions = data; 62 | 63 | _changeState(WalletStateSuccess()); 64 | }, 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/features/wallet/wallet_state.dart: -------------------------------------------------------------------------------- 1 | abstract class WalletState {} 2 | 3 | class WalletStateInitial extends WalletState {} 4 | 5 | class WalletStateLoading extends WalletState {} 6 | 7 | class WalletStateSuccess extends WalletState {} 8 | 9 | class WalletStateError extends WalletState { 10 | WalletStateError({required this.message}); 11 | 12 | final String message; 13 | } 14 | -------------------------------------------------------------------------------- /lib/firebase_options.dart: -------------------------------------------------------------------------------- 1 | // File generated by FlutterFire CLI. 2 | // ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members 3 | import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; 4 | import 'package:flutter/foundation.dart' 5 | show defaultTargetPlatform, kIsWeb, TargetPlatform; 6 | 7 | /// Default [FirebaseOptions] for use with your Firebase apps. 8 | /// 9 | /// Example: 10 | /// ```dart 11 | /// import 'firebase_options.dart'; 12 | /// // ... 13 | /// await Firebase.initializeApp( 14 | /// options: DefaultFirebaseOptions.currentPlatform, 15 | /// ); 16 | /// ``` 17 | class DefaultFirebaseOptions { 18 | static FirebaseOptions get currentPlatform { 19 | if (kIsWeb) { 20 | throw UnsupportedError( 21 | 'DefaultFirebaseOptions have not been configured for web - ' 22 | 'you can reconfigure this by running the FlutterFire CLI again.', 23 | ); 24 | } 25 | switch (defaultTargetPlatform) { 26 | case TargetPlatform.android: 27 | return android; 28 | case TargetPlatform.iOS: 29 | return ios; 30 | case TargetPlatform.macOS: 31 | throw UnsupportedError( 32 | 'DefaultFirebaseOptions have not been configured for macos - ' 33 | 'you can reconfigure this by running the FlutterFire CLI again.', 34 | ); 35 | case TargetPlatform.windows: 36 | throw UnsupportedError( 37 | 'DefaultFirebaseOptions have not been configured for windows - ' 38 | 'you can reconfigure this by running the FlutterFire CLI again.', 39 | ); 40 | case TargetPlatform.linux: 41 | throw UnsupportedError( 42 | 'DefaultFirebaseOptions have not been configured for linux - ' 43 | 'you can reconfigure this by running the FlutterFire CLI again.', 44 | ); 45 | default: 46 | throw UnsupportedError( 47 | 'DefaultFirebaseOptions are not supported for this platform.', 48 | ); 49 | } 50 | } 51 | 52 | static const FirebaseOptions android = FirebaseOptions( 53 | apiKey: 'AIzaSyCsMPPonxwD9ZstTs_5ha3wjBMpKSwmlvQ', 54 | appId: '1:539092104395:android:676a145bab4c03b626d74e', 55 | messagingSenderId: '539092104395', 56 | projectId: 'financy-app-6650d', 57 | storageBucket: 'financy-app-6650d.appspot.com', 58 | ); 59 | 60 | static const FirebaseOptions ios = FirebaseOptions( 61 | apiKey: 'AIzaSyDh3TdIA1yIJV1INRPwXhVQUlu44ctJSJ8', 62 | appId: '1:539092104395:ios:f1326e9eb37f99f126d74e', 63 | messagingSenderId: '539092104395', 64 | projectId: 'financy-app-6650d', 65 | storageBucket: 'financy-app-6650d.appspot.com', 66 | iosClientId: 67 | '539092104395-5rijk44jomtu8u2blt7fdd4ltk63d1o6.apps.googleusercontent.com', 68 | iosBundleId: 'dev.kaio.financy', 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_core/firebase_core.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'app.dart'; 5 | import 'firebase_options.dart'; 6 | import 'locator.dart'; 7 | 8 | void main() async { 9 | WidgetsFlutterBinding.ensureInitialized(); 10 | await Firebase.initializeApp( 11 | options: DefaultFirebaseOptions.currentPlatform, 12 | ); 13 | 14 | setupDependencies(); 15 | 16 | await locator.allReady(); 17 | 18 | runApp(const App()); 19 | } 20 | -------------------------------------------------------------------------------- /lib/repositories/repositories.dart: -------------------------------------------------------------------------------- 1 | export 'transaction_repository.dart'; 2 | export 'transaction_repository_impl.dart'; 3 | -------------------------------------------------------------------------------- /lib/repositories/transaction_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import '../common/data/data_result.dart'; 4 | import '../common/models/models.dart'; 5 | 6 | /// {@template transaction_repository} 7 | /// Communicates Transactions CRUD operations between Controllers and Data Sources 8 | /// {@endtemplate} 9 | abstract class TransactionRepository { 10 | static const transactionsPath = 'transactions'; 11 | static const balancesPath = 'balances'; 12 | static const localChanges = 'local_changes'; 13 | 14 | Future> addTransaction({ 15 | required TransactionModel transaction, 16 | required String userId, 17 | }); 18 | 19 | Future> updateTransaction(TransactionModel transaction); 20 | 21 | Future>> getLatestTransactions(); 22 | 23 | Future> deleteTransaction(TransactionModel transaction); 24 | 25 | Future> getBalances(); 26 | 27 | Future> updateBalance({ 28 | TransactionModel? oldTransaction, 29 | required TransactionModel newTransaction, 30 | }); 31 | 32 | Future>> getTransactionsByDateRange({ 33 | required DateTime startDate, 34 | required DateTime endDate, 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /lib/services/auth_service/auth_service.dart: -------------------------------------------------------------------------------- 1 | import '../../common/data/data.dart'; 2 | import '../../common/models/models.dart'; 3 | 4 | abstract class AuthService { 5 | Future> signUp({ 6 | String? name, 7 | required String email, 8 | required String password, 9 | }); 10 | 11 | Future> signIn({ 12 | required String email, 13 | required String password, 14 | }); 15 | 16 | Future signOut(); 17 | 18 | Future> userToken(); 19 | 20 | Future> forgotPassword(String email); 21 | } 22 | -------------------------------------------------------------------------------- /lib/services/auth_service/firebase_auth_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_functions/cloud_functions.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | 4 | import '../../common/data/data.dart'; 5 | import '../../common/models/models.dart'; 6 | import 'auth_service.dart'; 7 | 8 | class FirebaseAuthService implements AuthService { 9 | FirebaseAuthService() 10 | : _auth = FirebaseAuth.instance, 11 | _functions = FirebaseFunctions.instance; 12 | 13 | final FirebaseAuth _auth; 14 | final FirebaseFunctions _functions; 15 | 16 | @override 17 | Future> signIn({ 18 | required String email, 19 | required String password, 20 | }) async { 21 | try { 22 | final result = await _auth.signInWithEmailAndPassword( 23 | email: email, 24 | password: password, 25 | ); 26 | 27 | if (result.user != null) { 28 | return DataResult.success(_createUserModelFromAuthUser(result.user!)); 29 | } 30 | 31 | return DataResult.failure(const GeneralException()); 32 | } on FirebaseAuthException catch (e) { 33 | return DataResult.failure(AuthException(code: e.code)); 34 | } 35 | } 36 | 37 | @override 38 | Future> signUp({ 39 | String? name, 40 | required String email, 41 | required String password, 42 | }) async { 43 | try { 44 | await _functions.httpsCallable('registerUser').call({ 45 | "email": email, 46 | "password": password, 47 | "displayName": name, 48 | }); 49 | 50 | final result = await _auth.signInWithEmailAndPassword( 51 | email: email, 52 | password: password, 53 | ); 54 | 55 | if (result.user != null) { 56 | return DataResult.success(_createUserModelFromAuthUser(result.user!)); 57 | } 58 | 59 | return DataResult.failure(const GeneralException()); 60 | } on FirebaseAuthException catch (e) { 61 | return DataResult.failure(AuthException(code: e.code)); 62 | } on FirebaseFunctionsException catch (e) { 63 | return DataResult.failure(AuthException(code: e.code)); 64 | } 65 | } 66 | 67 | @override 68 | Future signOut() async { 69 | try { 70 | await _auth.signOut(); 71 | } catch (e) { 72 | rethrow; 73 | } 74 | } 75 | 76 | @override 77 | Future> userToken() async { 78 | try { 79 | final token = await _auth.currentUser?.getIdToken(); 80 | 81 | return DataResult.success(token ?? ''); 82 | } catch (e) { 83 | return DataResult.success(''); 84 | } 85 | } 86 | 87 | UserModel _createUserModelFromAuthUser(User user) { 88 | return UserModel( 89 | name: user.displayName, 90 | email: user.email, 91 | id: user.uid, 92 | ); 93 | } 94 | 95 | @override 96 | Future> forgotPassword(String email) async { 97 | try { 98 | await _auth.sendPasswordResetEmail(email: email); 99 | 100 | return DataResult.success(true); 101 | } on FirebaseAuthException catch (e) { 102 | return DataResult.failure(AuthException(code: e.code)); 103 | } catch (e) { 104 | return DataResult.failure(const GeneralException()); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/services/connection_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:http/http.dart'; 4 | 5 | class ConnectionService { 6 | const ConnectionService(); 7 | 8 | static bool _isConnected = false; 9 | 10 | bool get isConnected => _isConnected; 11 | 12 | Client get _client => Client(); 13 | 14 | Future checkConnection() async { 15 | try { 16 | final response = await _client.get(Uri.parse('https://example.com')); 17 | 18 | _isConnected = response.statusCode == 200; 19 | } catch (_) { 20 | _isConnected = false; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/services/data_service/data_service.dart: -------------------------------------------------------------------------------- 1 | abstract class DataService { 2 | Future create({ 3 | required String path, 4 | Map params = const {}, 5 | }); 6 | 7 | Future read({ 8 | required String path, 9 | Map params = const {}, 10 | }); 11 | 12 | Future update({ 13 | required String path, 14 | Map params = const {}, 15 | }); 16 | 17 | Future delete({ 18 | required String path, 19 | Map params = const {}, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /lib/services/secure_storage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 2 | 3 | class SecureStorageService { 4 | const SecureStorageService(); 5 | 6 | final _secureStorage = const FlutterSecureStorage(); 7 | 8 | Future write({required String key, String? value}) async { 9 | await _secureStorage.write( 10 | key: key, 11 | value: value, 12 | ); 13 | } 14 | 15 | Future readOne({required String key}) async { 16 | return await _secureStorage.read(key: key); 17 | } 18 | 19 | Future> readAll() async { 20 | return await _secureStorage.readAll(); 21 | } 22 | 23 | Future deleteOne({required String key}) async { 24 | await _secureStorage.delete(key: key); 25 | } 26 | 27 | Future deleteAll() async { 28 | await _secureStorage.deleteAll(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/services/services.dart: -------------------------------------------------------------------------------- 1 | export 'auth_service/auth_service.dart'; 2 | export 'auth_service/firebase_auth_service.dart'; 3 | export 'connection_service.dart'; 4 | export 'data_service/data_service.dart'; 5 | export 'data_service/database_service.dart'; 6 | export 'data_service/graphql_service.dart'; 7 | export 'secure_storage.dart'; 8 | export 'sync_service/sync_service.dart'; 9 | export 'user_data_service/user_data_service.dart'; 10 | export 'user_data_service/user_data_service_impl.dart'; 11 | -------------------------------------------------------------------------------- /lib/services/sync_service/sync_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | import 'sync_service.dart'; 4 | 5 | class SyncController extends ChangeNotifier { 6 | SyncController({required this.syncService}); 7 | 8 | final SyncService syncService; 9 | 10 | SyncState _state = SyncStateInitial(); 11 | 12 | SyncState get state => _state; 13 | 14 | void _changeState(SyncState newState) { 15 | _state = newState; 16 | notifyListeners(); 17 | } 18 | 19 | Future syncFromServer() async { 20 | _changeState(DownloadingDataFromServer()); 21 | 22 | final result = await syncService.syncFromServer(); 23 | 24 | result.fold( 25 | (error) => _changeState(DownloadDataFromServerError(error.message)), 26 | (_) => _changeState(DownloadedDataFromServer()), 27 | ); 28 | } 29 | 30 | Future syncToServer() async { 31 | _changeState(UploadingDataToServerState()); 32 | 33 | final result = await syncService.syncToServer(); 34 | 35 | result.fold( 36 | (error) => _changeState(UploadDataToServerError(error.message)), 37 | (_) => _changeState(UploadedDataToServer()), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/services/sync_service/sync_state.dart: -------------------------------------------------------------------------------- 1 | abstract class SyncState {} 2 | 3 | class SyncStateInitial extends SyncState {} 4 | 5 | class SyncStateLoading extends SyncState {} 6 | 7 | class UploadingDataToServerState extends SyncStateLoading {} 8 | 9 | class DownloadingDataFromServer extends SyncStateLoading {} 10 | 11 | class SyncStateError extends SyncState { 12 | SyncStateError(this.message); 13 | 14 | final String message; 15 | } 16 | 17 | class UploadDataToServerError extends SyncStateError { 18 | UploadDataToServerError(super.message); 19 | } 20 | 21 | class DownloadDataFromServerError extends SyncStateError { 22 | DownloadDataFromServerError(super.message); 23 | } 24 | 25 | class SyncStateSuccess extends SyncState {} 26 | 27 | class UploadedDataToServer extends SyncStateSuccess {} 28 | 29 | class DownloadedDataFromServer extends SyncStateSuccess {} 30 | -------------------------------------------------------------------------------- /lib/services/user_data_service/user_data_service.dart: -------------------------------------------------------------------------------- 1 | import '../../common/data/data_result.dart'; 2 | import '../../common/models/models.dart'; 3 | 4 | abstract class UserDataService { 5 | Future> getUserData(); 6 | 7 | Future> updateUserName(String newUserName); 8 | 9 | Future> updatePassword(String newPassword); 10 | 11 | Future> deleteAccount(); 12 | 13 | UserModel get userData; 14 | } 15 | -------------------------------------------------------------------------------- /test/common/widgets/primary_button_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:financy_app/common/widgets/primary_button.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | void main() { 8 | testWidgets( 9 | 'Should check if button text is correctly placed', 10 | (WidgetTester tester) async { 11 | Widget buildFrame() { 12 | return Directionality( 13 | textDirection: TextDirection.ltr, 14 | child: PrimaryButton( 15 | onPressed: () => log('pressed'), 16 | text: 'button', 17 | ), 18 | ); 19 | } 20 | 21 | await tester.pumpWidget(buildFrame()); 22 | expect(find.text('button'), findsOneWidget); 23 | }, 24 | ); 25 | testWidgets( 26 | 'Should check if onPressed callback is called when non-null', 27 | (WidgetTester tester) async { 28 | bool wasPressed; 29 | Finder primaryButton; 30 | 31 | Widget buildFrame({VoidCallback? onPressed}) { 32 | return Directionality( 33 | textDirection: TextDirection.ltr, 34 | child: PrimaryButton( 35 | onPressed: onPressed, 36 | text: 'button', 37 | ), 38 | ); 39 | } 40 | 41 | wasPressed = false; 42 | await tester.pumpWidget( 43 | buildFrame(onPressed: () { 44 | wasPressed = true; 45 | }), 46 | ); 47 | primaryButton = find.byType(PrimaryButton); 48 | expect(primaryButton, findsOneWidget); 49 | await tester.tap(primaryButton); 50 | expect(wasPressed, true); 51 | 52 | wasPressed = false; 53 | await tester.pumpWidget( 54 | buildFrame(), 55 | ); 56 | primaryButton = find.byType(PrimaryButton); 57 | await tester.tap(primaryButton); 58 | expect(wasPressed, false); 59 | }, 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /test/features/forgot_password/forgot_password_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/common/data/data.dart'; 2 | import 'package:financy_app/features/forgot_password/forgot_password_controller.dart'; 3 | import 'package:financy_app/features/forgot_password/forgot_password_state.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:mocktail/mocktail.dart'; 6 | 7 | import '../../mock/mock_classes.dart'; 8 | 9 | void main() { 10 | // Mocking dependencies to test SUT 11 | late MockFirebaseAuthService mockFirebaseAuthService; 12 | 13 | // Subject Under Test 14 | late ForgotPasswordController sut; 15 | 16 | setUp(() { 17 | mockFirebaseAuthService = MockFirebaseAuthService(); 18 | 19 | sut = ForgotPasswordController( 20 | authService: mockFirebaseAuthService, 21 | ); 22 | }); 23 | 24 | group('Tests Forgot Password Controller State', () { 25 | test('Should update state to ForgotPasswordStateSuccess', () async { 26 | expect(sut.state, isInstanceOf()); 27 | 28 | when(() => mockFirebaseAuthService.forgotPassword(any())).thenAnswer( 29 | (_) async => DataResult.success(true), 30 | ); 31 | 32 | await sut.forgotPassword('user@email.com'); 33 | 34 | expect(sut.state, isInstanceOf()); 35 | }); 36 | }); 37 | 38 | test('Should update state to ForgotPasswordStateError', () async { 39 | expect(sut.state, isInstanceOf()); 40 | 41 | when(() => mockFirebaseAuthService.forgotPassword(any())) 42 | .thenAnswer((_) async => DataResult.failure(const GeneralException())); 43 | 44 | await sut.forgotPassword('user@email.com'); 45 | 46 | expect(sut.state, isInstanceOf()); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /test/features/home/home_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/common/data/data.dart'; 2 | import 'package:financy_app/common/models/models.dart'; 3 | import 'package:financy_app/features/home/home_controller.dart'; 4 | import 'package:financy_app/features/home/home_state.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:mocktail/mocktail.dart'; 7 | 8 | import '../../mock/mock_classes.dart'; 9 | 10 | void main() { 11 | late MockTransactionRepository mockTransactionRepository; 12 | late MockSyncService mockSyncService; 13 | late MockUserDataService mockUserDataService; 14 | 15 | late HomeController sut; 16 | late List transactions; 17 | 18 | setUp(() { 19 | mockSyncService = MockSyncService(); 20 | mockTransactionRepository = MockTransactionRepository(); 21 | mockUserDataService = MockUserDataService(); 22 | 23 | sut = HomeController( 24 | transactionRepository: mockTransactionRepository, 25 | userDataService: mockUserDataService, 26 | ); 27 | 28 | transactions = [ 29 | TransactionModel.fromMap({ 30 | 'id': '1', 31 | 'title': 'title', 32 | 'description': 'description', 33 | 'value': 10.0, 34 | 'date': DateTime.now().toIso8601String(), 35 | 'created_at': DateTime.now().toIso8601String(), 36 | 'status': false, 37 | 'type': 'expense', 38 | 'category': 'category', 39 | 'user_id': '1' 40 | }), 41 | ]; 42 | 43 | when(() => mockSyncService.syncFromServer()) 44 | .thenAnswer((_) async => DataResult.success(null)); 45 | }); 46 | 47 | group('Tests Home Controller State', () { 48 | test(''' 49 | \nGiven: that the initial state is HomeStateInitial 50 | When: getLatestTransactions is called and returns transactions 51 | Then: HomeState should be HomeStateSuccess 52 | ''', () async { 53 | expect(sut.state, isInstanceOf()); 54 | expect(sut.transactions, isEmpty); 55 | 56 | when(() => mockTransactionRepository.getLatestTransactions()).thenAnswer( 57 | (_) async => DataResult.success(transactions), 58 | ); 59 | 60 | await sut.getLatestTransactions(); 61 | 62 | expect(sut.transactions, isNotEmpty); 63 | 64 | expect(sut.state, isInstanceOf()); 65 | }); 66 | 67 | test(''' 68 | \nGiven: that the initial state is HomeStateInitial 69 | When: getLatestTransactions is called and returns failure 70 | Then: HomeState should be HomeStateError 71 | ''', () async { 72 | expect(sut.state, isInstanceOf()); 73 | expect(sut.transactions, isEmpty); 74 | 75 | when(() => mockTransactionRepository.getLatestTransactions()).thenAnswer( 76 | (_) async => DataResult.failure(const GeneralException()), 77 | ); 78 | 79 | await sut.getLatestTransactions(); 80 | 81 | expect(sut.transactions, isEmpty); 82 | 83 | expect(sut.state, isInstanceOf()); 84 | }); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /test/features/profile/profile_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/common/data/data_result.dart'; 2 | import 'package:financy_app/common/data/exceptions.dart'; 3 | import 'package:financy_app/common/models/models.dart'; 4 | import 'package:financy_app/features/profile/profile_controller.dart'; 5 | import 'package:financy_app/features/profile/profile_state.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:mocktail/mocktail.dart'; 8 | 9 | import '../../mock/mock_classes.dart'; 10 | 11 | void main() { 12 | late ProfileController sut; 13 | late MockUserDataService userDataService; 14 | setUp(() { 15 | userDataService = MockUserDataService(); 16 | sut = ProfileController(userDataService: userDataService); 17 | }); 18 | 19 | group('Tests ProfileController State', () { 20 | test('Initial state should be ProfileInitialState', () { 21 | expect(sut.state, isA()); 22 | }); 23 | 24 | test( 25 | 'When getUserData is called and return success, state should be ProfileStateSuccess', 26 | () async { 27 | when(() => userDataService.getUserData()).thenAnswer( 28 | (_) async => DataResult.success( 29 | UserModel( 30 | email: 'user@email.com', 31 | name: 'User', 32 | id: '123', 33 | ), 34 | ), 35 | ); 36 | 37 | await sut.getUserData(); 38 | 39 | expect(sut.state, isA()); 40 | expect((sut.state as ProfileStateSuccess).user, isA()); 41 | expect((sut.state as ProfileStateSuccess).user?.email, 'user@email.com'); 42 | expect((sut.state as ProfileStateSuccess).user?.name, 'User'); 43 | expect((sut.state as ProfileStateSuccess).user?.id, '123'); 44 | }); 45 | 46 | test( 47 | 'When getUserData is called and return failure, state should be ProfileStateFailure', 48 | () async { 49 | when(() => userDataService.getUserData()).thenAnswer( 50 | (_) async => 51 | DataResult.failure(const UserDataException(code: 'not-found')), 52 | ); 53 | 54 | await sut.getUserData(); 55 | 56 | expect(sut.state, isA()); 57 | expect((sut.state as ProfileStateError).message, 58 | 'User data not found. Please login again.'); 59 | }); 60 | 61 | test( 62 | 'When deleteAccount is called and return success, state should be ProfileStateSuccess', 63 | () async { 64 | when(() => userDataService.deleteAccount()) 65 | .thenAnswer((_) async => DataResult.success(true)); 66 | 67 | await sut.deleteAccount(); 68 | 69 | expect(sut.state, isA()); 70 | }); 71 | 72 | test( 73 | 'When deleteAccount is called and return failure, state should be ProfileStateFailure', 74 | () async { 75 | when(() => userDataService.deleteAccount()).thenAnswer( 76 | (_) async => 77 | DataResult.failure(const UserDataException(code: 'delete-account')), 78 | ); 79 | 80 | await sut.deleteAccount(); 81 | 82 | expect(sut.state, isA()); 83 | expect((sut.state as ProfileStateError).message, 84 | 'An error has occurred while deleting user account. Please try again later.'); 85 | }); 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /test/features/sign_in/sign_in_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/common/data/data_result.dart'; 2 | import 'package:financy_app/common/data/exceptions.dart'; 3 | import 'package:financy_app/common/models/user_model.dart'; 4 | import 'package:financy_app/features/sign_in/sign_in_controller.dart'; 5 | import 'package:financy_app/features/sign_in/sign_in_state.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:mocktail/mocktail.dart'; 8 | 9 | import '../../mock/mock_classes.dart'; 10 | 11 | void main() { 12 | //Mocking dependencies to test SUT 13 | late MockSecureStorageService mockSecureStorage; 14 | late MockFirebaseAuthService mockFirebaseAuthService; 15 | late MockSyncService mockSyncService; 16 | 17 | //Subject Under Test 18 | late SignInController sut; 19 | 20 | late UserModel user; 21 | 22 | setUp(() { 23 | mockSecureStorage = MockSecureStorageService(); 24 | mockFirebaseAuthService = MockFirebaseAuthService(); 25 | mockSyncService = MockSyncService(); 26 | 27 | sut = SignInController( 28 | authService: mockFirebaseAuthService, 29 | secureStorageService: mockSecureStorage, 30 | ); 31 | 32 | user = UserModel( 33 | name: 'User', 34 | email: 'user@email.com', 35 | id: '1a2b3c4d5e', 36 | ); 37 | 38 | when(() => mockSyncService.syncFromServer()) 39 | .thenAnswer((_) async => DataResult.success(null)); 40 | 41 | when(() => mockSecureStorage.write( 42 | key: "CURRENT_USER", 43 | value: user.toJson(), 44 | )).thenAnswer((_) async {}); 45 | }); 46 | 47 | group('Tests Sign In Controller State', () { 48 | test('Should update state to SignInStateSuccess', () async { 49 | expect(sut.state, isInstanceOf()); 50 | 51 | when( 52 | () => mockFirebaseAuthService.signIn( 53 | email: 'user@email.com', 54 | password: 'user@123', 55 | ), 56 | ).thenAnswer( 57 | (_) async => DataResult.success(user), 58 | ); 59 | 60 | await sut.signIn( 61 | email: 'user@email.com', 62 | password: 'user@123', 63 | ); 64 | 65 | expect(sut.state, isInstanceOf()); 66 | }); 67 | 68 | test('Should update state to SignInStateError', () async { 69 | expect(sut.state, isInstanceOf()); 70 | 71 | when( 72 | () => mockFirebaseAuthService.signIn( 73 | email: 'user@email.com', 74 | password: 'user@123', 75 | ), 76 | ).thenAnswer((_) async => DataResult.failure(const GeneralException())); 77 | 78 | await sut.signIn( 79 | email: 'user@email.com', 80 | password: 'user@123', 81 | ); 82 | 83 | expect(sut.state, isInstanceOf()); 84 | }); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /test/features/sign_up/sign_up_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/common/data/data_result.dart'; 2 | import 'package:financy_app/common/data/exceptions.dart'; 3 | import 'package:financy_app/common/models/user_model.dart'; 4 | import 'package:financy_app/features/sign_up/sign_up_controller.dart'; 5 | import 'package:financy_app/features/sign_up/sign_up_state.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:mocktail/mocktail.dart'; 8 | 9 | import '../../mock/mock_classes.dart'; 10 | 11 | void main() { 12 | late MockSecureStorageService mockSecureStorage; 13 | late MockFirebaseAuthService mockFirebaseAuthService; 14 | 15 | late SignUpController sut; 16 | 17 | late UserModel user; 18 | 19 | setUp(() { 20 | mockFirebaseAuthService = MockFirebaseAuthService(); 21 | mockSecureStorage = MockSecureStorageService(); 22 | 23 | sut = SignUpController( 24 | authService: mockFirebaseAuthService, 25 | secureStorageService: mockSecureStorage, 26 | ); 27 | 28 | user = UserModel( 29 | name: 'User', 30 | email: 'user@email.com', 31 | id: '1a2b3c4d5e', 32 | ); 33 | 34 | when(() => mockSecureStorage.write( 35 | key: "CURRENT_USER", 36 | value: user.toJson(), 37 | )).thenAnswer((_) async {}); 38 | }); 39 | 40 | group('Tests Sign Up Controller State', () { 41 | test('Should update state to SignUpStateSuccess', () async { 42 | expect(sut.state, isInstanceOf()); 43 | 44 | when( 45 | () => mockFirebaseAuthService.signUp( 46 | name: 'User', 47 | email: 'user@email.com', 48 | password: 'user@123', 49 | ), 50 | ).thenAnswer( 51 | (_) async => DataResult.success(user), 52 | ); 53 | 54 | when(() => mockSecureStorage.write( 55 | key: "CURRENT_USER", 56 | value: user.toJson(), 57 | )).thenAnswer((_) async {}); 58 | 59 | await sut.signUp( 60 | name: 'User', 61 | email: 'user@email.com', 62 | password: 'user@123', 63 | ); 64 | 65 | expect(sut.state, isInstanceOf()); 66 | 67 | await Future.delayed(Duration.zero); 68 | 69 | expect(sut.state, isInstanceOf()); 70 | }); 71 | 72 | test('Should update state to SignUpStateError', () async { 73 | expect(sut.state, isInstanceOf()); 74 | 75 | when( 76 | () => mockFirebaseAuthService.signUp( 77 | name: 'User', 78 | email: 'user@email.com', 79 | password: 'user@123', 80 | ), 81 | ).thenAnswer((_) async => DataResult.failure(const GeneralException())); 82 | 83 | await sut.signUp( 84 | name: 'User', 85 | email: 'user@email.com', 86 | password: 'user@123', 87 | ); 88 | 89 | expect(sut.state, isInstanceOf()); 90 | }); 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /test/features/splash/splash_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/common/data/data.dart'; 2 | import 'package:financy_app/common/models/user_model.dart'; 3 | import 'package:financy_app/features/splash/splash_controller.dart'; 4 | import 'package:financy_app/features/splash/splash_state.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:mocktail/mocktail.dart'; 7 | 8 | import '../../mock/mock_classes.dart'; 9 | 10 | void main() { 11 | late MockSyncService mockSyncService; 12 | late MockSecureStorageService mockSecureStorage; 13 | late SplashController splashController; 14 | late UserModel user; 15 | 16 | setUp(() { 17 | mockSecureStorage = MockSecureStorageService(); 18 | mockSyncService = MockSyncService(); 19 | splashController = SplashController( 20 | secureStorageService: mockSecureStorage, 21 | ); 22 | user = UserModel( 23 | name: 'User', 24 | email: 'user@email.com', 25 | id: '1a2b3c4d5e', 26 | ); 27 | 28 | when(() => mockSyncService.syncFromServer()) 29 | .thenAnswer((_) async => DataResult.success(null)); 30 | when(() => mockSyncService.syncToServer()) 31 | .thenAnswer((_) async => DataResult.success(null)); 32 | }); 33 | 34 | group('Tests Splash Controller', () { 35 | test('Should update state to UnauthenticatedUser', () async { 36 | when(() => mockSecureStorage.readOne(key: 'CURRENT_USER')) 37 | .thenAnswer((_) async => null); 38 | 39 | expect(splashController.state, isInstanceOf()); 40 | 41 | await splashController.isUserLogged(); 42 | 43 | expect(splashController.state, isInstanceOf()); 44 | }); 45 | test('Should update state to AuthenticatedUser', () async { 46 | when(() => mockSecureStorage.readOne(key: 'CURRENT_USER')) 47 | .thenAnswer((_) async => user.toJson()); 48 | 49 | expect(splashController.state, isInstanceOf()); 50 | 51 | await splashController.isUserLogged(); 52 | 53 | expect(splashController.state, isInstanceOf()); 54 | }); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /test/features/wallet/wallet_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/common/data/data.dart'; 2 | import 'package:financy_app/common/models/models.dart'; 3 | import 'package:financy_app/features/wallet/wallet_controller.dart'; 4 | import 'package:financy_app/features/wallet/wallet_state.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:mocktail/mocktail.dart'; 7 | 8 | import '../../mock/mock_classes.dart'; 9 | 10 | void main() { 11 | late MockTransactionRepository mockTransactionRepository; 12 | 13 | late WalletController sut; 14 | late List transactions; 15 | 16 | setUp(() { 17 | mockTransactionRepository = MockTransactionRepository(); 18 | 19 | sut = WalletController( 20 | transactionRepository: mockTransactionRepository, 21 | ); 22 | 23 | transactions = [ 24 | TransactionModel.fromMap({ 25 | 'id': '1', 26 | 'title': 'title', 27 | 'description': 'description', 28 | 'value': 10.0, 29 | 'date': DateTime.now().toIso8601String(), 30 | 'created_at': DateTime.now().toIso8601String(), 31 | 'status': false, 32 | 'type': 'expense', 33 | 'category': 'category', 34 | 'user_id': '1' 35 | }), 36 | ]; 37 | }); 38 | 39 | group('Tests Wallet Controller State', () { 40 | test(''' 41 | \nGiven: that the initial state is WalletStateInitial 42 | When: getAllTransactions is called and returns transactions 43 | Then: WalletState should be WalletStateSuccess 44 | ''', () async { 45 | expect(sut.state, isInstanceOf()); 46 | expect(sut.transactions, isEmpty); 47 | 48 | when(() => mockTransactionRepository.getLatestTransactions()).thenAnswer( 49 | (_) async => DataResult.success(transactions), 50 | ); 51 | 52 | await sut.getAllTransactions(); 53 | 54 | expect(sut.transactions, isNotEmpty); 55 | 56 | expect(sut.state, isInstanceOf()); 57 | }); 58 | 59 | test(''' 60 | \nGiven: that the initial state is WalletStateInitial 61 | When: getAllTransactions is called and returns failure 62 | Then: WalletState should be WalletStateError 63 | ''', () async { 64 | expect(sut.state, isInstanceOf()); 65 | expect(sut.transactions, isEmpty); 66 | 67 | when(() => mockTransactionRepository.getLatestTransactions()).thenAnswer( 68 | (_) async => DataResult.failure(const GeneralException()), 69 | ); 70 | 71 | await sut.getAllTransactions(); 72 | 73 | expect(sut.transactions, isEmpty); 74 | 75 | expect(sut.state, isInstanceOf()); 76 | }); 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /test/mock/mock_classes.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_functions/cloud_functions.dart'; 2 | import 'package:financy_app/common/models/user_model.dart'; 3 | import 'package:financy_app/repositories/repositories.dart'; 4 | import 'package:financy_app/services/services.dart'; 5 | import 'package:firebase_auth/firebase_auth.dart'; 6 | import 'package:mocktail/mocktail.dart'; 7 | 8 | // Mock Models 9 | 10 | class MockUser extends Mock implements UserModel {} 11 | 12 | // Mock Services 13 | class MockFirebaseAuthService extends Mock implements AuthService {} 14 | 15 | class MockSecureStorageService extends Mock implements SecureStorageService {} 16 | 17 | class MockDatabaseService extends Mock implements DatabaseService {} 18 | 19 | class MockGraphQLService extends Mock implements GraphQLService {} 20 | 21 | class MockSyncService extends Mock implements SyncService {} 22 | 23 | class MockUserDataService extends Mock implements UserDataService {} 24 | 25 | // Mock Repositories 26 | 27 | class MockTransactionRepository extends Mock implements TransactionRepository {} 28 | 29 | // Mock FirebaseAuth 30 | 31 | class MockFirebaseAuth extends Mock implements FirebaseAuth {} 32 | 33 | class FakeUser extends Fake implements User { 34 | @override 35 | String get uid => '123456abc'; 36 | 37 | @override 38 | String get displayName => 'User'; 39 | 40 | @override 41 | String get email => 'user@email.com'; 42 | } 43 | 44 | // Mock FirebaseFunctions 45 | 46 | class MockFirebaseFunctions extends Mock implements FirebaseFunctions {} 47 | -------------------------------------------------------------------------------- /test/services/firebase_auth_service_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/common/data/data.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mocktail/mocktail.dart'; 4 | 5 | import '../mock/mock_classes.dart'; 6 | 7 | void main() { 8 | late MockFirebaseAuthService mockFirebaseAuthService; 9 | late MockUser user; 10 | setUp(() { 11 | mockFirebaseAuthService = MockFirebaseAuthService(); 12 | user = MockUser(); 13 | }); 14 | 15 | group( 16 | 'Tests Firebase Auth Service - Sign Up', 17 | () { 18 | test('Should return created user', () async { 19 | when( 20 | () => mockFirebaseAuthService.signUp( 21 | name: 'User', 22 | email: 'user@email.com', 23 | password: 'user@123', 24 | ), 25 | ).thenAnswer( 26 | (_) async => DataResult.success(user), 27 | ); 28 | 29 | final result = await mockFirebaseAuthService.signUp( 30 | name: 'User', 31 | email: 'user@email.com', 32 | password: 'user@123', 33 | ); 34 | 35 | expect( 36 | result.data, 37 | user, 38 | ); 39 | }); 40 | 41 | test('Should throw exception', () async { 42 | when( 43 | () => mockFirebaseAuthService.signUp( 44 | name: 'User', 45 | email: 'user@email.com', 46 | password: 'user@123', 47 | ), 48 | ).thenThrow( 49 | Exception(), 50 | ); 51 | 52 | expect( 53 | () => mockFirebaseAuthService.signUp( 54 | name: 'User', 55 | email: 'user@email.com', 56 | password: 'user@123', 57 | ), 58 | // throwsA(isInstanceOf()), 59 | throwsException, 60 | ); 61 | }); 62 | }, 63 | ); 64 | 65 | group('Tests Firebase Auth Service - Sign In', () { 66 | test('Should return user data', () async { 67 | when( 68 | () => mockFirebaseAuthService.signIn( 69 | email: 'user@email.com', 70 | password: 'user@123', 71 | ), 72 | ).thenAnswer( 73 | (_) async => DataResult.success(user), 74 | ); 75 | 76 | final result = await mockFirebaseAuthService.signIn( 77 | email: 'user@email.com', 78 | password: 'user@123', 79 | ); 80 | 81 | expect( 82 | result.data, 83 | user, 84 | ); 85 | }); 86 | 87 | test('Should throw exception', () async { 88 | when( 89 | () => mockFirebaseAuthService.signIn( 90 | email: 'user@email.com', 91 | password: 'user@123', 92 | ), 93 | ).thenAnswer((_) async => DataResult.failure(const GeneralException())); 94 | 95 | final result = await mockFirebaseAuthService.signIn( 96 | email: 'user@email.com', 97 | password: 'user@123', 98 | ); 99 | 100 | result.fold( 101 | (error) => expect(error, isA()), 102 | (data) => expect(data, null), 103 | ); 104 | }); 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /test/services/sync_service/sync_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:financy_app/common/data/data.dart'; 2 | import 'package:financy_app/services/sync_service/sync_service.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:mocktail/mocktail.dart'; 5 | 6 | import '../../mock/mock_classes.dart'; 7 | 8 | void main() { 9 | late MockSyncService mockSyncService; 10 | late SyncController sut; 11 | 12 | setUp(() { 13 | mockSyncService = MockSyncService(); 14 | sut = SyncController(syncService: mockSyncService); 15 | }); 16 | 17 | group('Tests SyncController State', () { 18 | group('when syncFromServer is called', () { 19 | test( 20 | '\n' 21 | 'GIVEN: that the user has internet connection' 22 | '\n' 23 | 'WHEN: the sync data from server is called and the data download is successful' 24 | '\n' 25 | 'THEN: the state must change to DownloadedDataFromServer', () async { 26 | // Arrange 27 | expect(sut.state, isA()); 28 | 29 | when(() => mockSyncService.syncFromServer()) 30 | .thenAnswer((_) async => DataResult.success(null)); 31 | 32 | await sut.syncFromServer(); 33 | expect(sut.state, isA()); 34 | }); 35 | 36 | test( 37 | '\n' 38 | 'GIVEN: that the user has internet connection' 39 | '\n' 40 | 'WHEN: the sync data from server is called and the data download is unsuccessful' 41 | '\n' 42 | 'THEN: the state must change to DownloadDataFromServerError', 43 | () async { 44 | // Arrange 45 | expect(sut.state, isA()); 46 | 47 | when(() => mockSyncService.syncFromServer()).thenAnswer((_) async => 48 | DataResult.failure(const SyncException(code: 'error'))); 49 | 50 | await sut.syncFromServer(); 51 | expect(sut.state, isA()); 52 | }); 53 | }); 54 | 55 | group('when syntToServer is called', () { 56 | test( 57 | '\n' 58 | 'GIVEN: that the user has internet connection' 59 | '\n' 60 | 'WHEN: the sync data to server is called and the data upload is successful' 61 | '\n' 62 | 'THEN: the state must change to UploadedDataToServer', () async { 63 | // Arrange 64 | expect(sut.state, isA()); 65 | 66 | when(() => mockSyncService.syncToServer()) 67 | .thenAnswer((_) async => DataResult.success(null)); 68 | 69 | await sut.syncToServer(); 70 | expect(sut.state, isA()); 71 | }); 72 | 73 | test( 74 | '\n' 75 | 'GIVEN: that the user has internet connection' 76 | '\n' 77 | 'WHEN: the sync data to server is called and the data upload is unsuccessful' 78 | '\n' 79 | 'THEN: the state must change to UploadDataToServerError', () async { 80 | // Arrange 81 | expect(sut.state, isA()); 82 | 83 | when(() => mockSyncService.syncToServer()).thenAnswer((_) async => 84 | DataResult.failure(const SyncException(code: 'error'))); 85 | 86 | await sut.syncToServer(); 87 | expect(sut.state, isA()); 88 | }); 89 | }); 90 | }); 91 | } 92 | --------------------------------------------------------------------------------