├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── tarekalabd │ │ │ │ ├── flutter_steps_tracker │ │ │ │ └── MainActivity.kt │ │ │ │ └── stepstracker │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── images │ ├── man-intro.jpeg │ └── rewards.png └── screenshots │ ├── 1.png │ ├── 2-1.PNG │ ├── 2.PNG │ ├── 3-1.PNG │ ├── 3.PNG │ ├── 4-1.PNG │ ├── 4.PNG │ ├── 5-1.PNG │ └── 5.PNG ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── core │ ├── data │ │ ├── data_sources │ │ │ ├── cache_helper.dart │ │ │ ├── data_sources_body.dart │ │ │ └── database.dart │ │ ├── error │ │ │ ├── exceptions │ │ │ │ ├── application_exception.dart │ │ │ │ ├── firebase_auth_exception_app.dart │ │ │ │ └── firebase_auth_exception_app.freezed.dart │ │ │ └── failures │ │ │ │ ├── application_failure.dart │ │ │ │ ├── firebase_auth_failure.dart │ │ │ │ └── firebase_auth_failure.freezed.dart │ │ ├── models │ │ │ ├── steps_and_points_model.dart │ │ │ └── user_model.dart │ │ └── services │ │ │ └── firestore_services.dart │ ├── domain │ │ └── use_cases │ │ │ └── use_case.dart │ └── presentation │ │ ├── pages │ │ └── landing_page.dart │ │ └── widgets │ │ ├── android_dialog.dart │ │ ├── ios_dialog.dart │ │ ├── show_alert_dialog.dart │ │ └── show_exception_alert_dialog.dart ├── di │ ├── app_module.dart │ ├── injection_container.config.dart │ └── injection_container.dart ├── features │ ├── bottom_navbar │ │ ├── data │ │ │ ├── mappers │ │ │ │ └── user_model_to_entity_mapper.dart │ │ │ ├── models │ │ │ │ ├── exchange_history_model.dart │ │ │ │ └── reward_model.dart │ │ │ └── repositories │ │ │ │ └── bottom_navbar_repository_impl.dart │ │ ├── domain │ │ │ ├── entities │ │ │ │ └── leaderboard_item_entity.dart │ │ │ ├── repositories │ │ │ │ └── bottom_navbar_repository.dart │ │ │ └── use_cases │ │ │ │ ├── earn_reward_use_case.dart │ │ │ │ ├── get_exchanges_history_use_case.dart │ │ │ │ ├── get_rewards_use_case.dart │ │ │ │ ├── get_user_data_use_case.dart │ │ │ │ ├── get_users_use_case.dart │ │ │ │ ├── set_exchange_history_use_case.dart │ │ │ │ └── set_steps_and_points_use_case.dart │ │ └── presentation │ │ │ ├── manager │ │ │ ├── exchanges_history │ │ │ │ ├── exchanges_history_cubit.dart │ │ │ │ ├── exchanges_history_state.dart │ │ │ │ └── exchanges_history_state.freezed.dart │ │ │ ├── home │ │ │ │ ├── home_cubit.dart │ │ │ │ ├── home_state.dart │ │ │ │ └── home_state.freezed.dart │ │ │ ├── leaderboard │ │ │ │ ├── leaderboard_cubit.dart │ │ │ │ ├── leaderboard_state.dart │ │ │ │ └── leaderboard_state.freezed.dart │ │ │ └── rewards │ │ │ │ ├── rewards_cubit.dart │ │ │ │ ├── rewards_state.dart │ │ │ │ └── rewards_state.freezed.dart │ │ │ ├── pages │ │ │ ├── bottom_navbar.dart │ │ │ ├── exchanges_page.dart │ │ │ ├── home_page.dart │ │ │ ├── leaderboard_page.dart │ │ │ └── rewards_page.dart │ │ │ └── widgets │ │ │ ├── ad_area.dart │ │ │ ├── app_bar_area.dart │ │ │ ├── exchanges_item.dart │ │ │ ├── get_radial_gauge.dart │ │ │ ├── health_points_and_calories.dart │ │ │ ├── health_points_and_calories_item.dart │ │ │ ├── leaderboard_item.dart │ │ │ ├── leaderboard_top_item.dart │ │ │ └── rewards_item.dart │ └── intro │ │ ├── data │ │ ├── data_sources │ │ │ ├── auth_local_data_source.dart │ │ │ └── auth_remote_data_source.dart │ │ ├── mappers │ │ │ └── user_entity_to_model_mapper.dart │ │ ├── repositories │ │ │ └── auth_repository_impl.dart │ │ └── services │ │ │ └── auth_services.dart │ │ ├── domain │ │ ├── entities │ │ │ └── user_entity.dart │ │ ├── repositories │ │ │ └── auth_repository.dart │ │ └── use_cases │ │ │ ├── auth_status_use_case.dart │ │ │ └── sign_in_anonymously_use_case.dart │ │ └── presentation │ │ ├── manager │ │ ├── auth_actions │ │ │ ├── auth_cubit.dart │ │ │ ├── auth_state.dart │ │ │ └── auth_state.freezed.dart │ │ └── auth_status │ │ │ ├── auth_status_cubit.dart │ │ │ ├── auth_status_state.dart │ │ │ └── auth_status_state.freezed.dart │ │ └── pages │ │ └── intro_page.dart ├── generated │ ├── intl │ │ ├── messages_all.dart │ │ ├── messages_ar.dart │ │ └── messages_en.dart │ └── l10n.dart ├── l10n │ ├── intl_ar.arb │ └── intl_en.arb ├── main.dart └── utilities │ ├── constants │ ├── api_path.dart │ ├── app_colors.dart │ ├── assets.dart │ ├── enums.dart │ └── key_constants.dart │ ├── locale │ ├── cubit │ │ ├── utility_cubit.dart │ │ ├── utility_state.dart │ │ └── utility_state.freezed.dart │ └── theme_data.dart │ └── routes │ ├── router.dart │ └── routes.dart ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Web related 36 | lib/generated_plugin_registrant.dart 37 | 38 | # Symbolication related 39 | app.*.symbols 40 | 41 | # Obfuscation related 42 | app.*.map.json 43 | 44 | # Android Studio will place build artifacts here 45 | /android/app/debug 46 | /android/app/profile 47 | /android/app/release 48 | -------------------------------------------------------------------------------- /.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: cd41fdd495f6944ecd3506c21e94c6567b073278 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: cd41fdd495f6944ecd3506c21e94c6567b073278 17 | base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 18 | - platform: android 19 | create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 20 | base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 21 | - platform: ios 22 | create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 23 | base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 24 | - platform: linux 25 | create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 26 | base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 27 | - platform: macos 28 | create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 29 | base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 30 | - platform: web 31 | create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 32 | base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 33 | - platform: windows 34 | create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 35 | base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Steps Tracker 2 | 3 | It's an open source project for the steps tracking (Pedometer) built with Flutter and integrated with Firebase as the initial backend service. 4 | 5 | Some of the features should have been in the server-side using cloud functions, but there were some problems with that and it will be available soon with some configuration instructions to work on one server. 6 | 7 | So you may find some workarounds that will be more accurate and better after adding the server-side functions. 8 | 9 | ## Quick Start 🚀 10 | 11 | ## Feature Set ✨ 12 | 13 | * [x] Free & Open Source 14 | * [x] Cross Platform App (Android - IOS) 15 | * [x] Usable and user-friendly interface 16 | * [x] Proper architecture for the code, Clean Architecture 17 | * [x] Authentication Anonymously with Firebase 18 | * [x] Recording the users data in Firestore Database 19 | * [x] Tracking the user footsteps and update them in real-time manner while the app is in the foreground 20 | * [x] For every 100 steps, a function adds 5 points to the health points of the user 21 | * [x] Showing a visual feedback to the user (now it's simple, Snackbar) to notice the users that they gain extra points 22 | * [x] Catalog of rewards, so you can pick a reward you like 23 | * [x] Rewards are paid with the health points, just scan the QR code (now it's dummy) and confirm if you have enough number of points 24 | * [x] History that lists all the exchanges to health points and the rewards taken 25 | * [x] Leaderboard page where you can see your ranking between all the users 26 | * [x] Multilingual, supports both Arabic and English 27 | * [x] Multi themes, supports the light and dark theme 28 | 29 | ## Future Steps ✨ 30 | 31 | * [ ] Refactor some of the colors, methods, cubits, repos and some files in the architecture 32 | * [ ] Daily steps (The Pedometer plugin basically calculates the total steps not daily) 33 | * [ ] Customized Goals 34 | * [ ] Build a good UI for the empty states (one empty state) 35 | * [ ] Integrate with the cloud functions and make the app more accurate 36 | * [ ] Update the visual feedback after the user gains extra points 37 | * [ ] Make the app works on the background 38 | * [ ] Enable push notifications 39 | * [ ] Sends notifications as the visual feedback for gaining more points (Background) 40 | * [ ] CI/CD for reviewing the pull requests 41 | * [ ] Publish the app to Google Play Store 42 | 43 | ## Screenshots :camera: 44 | 45 | 46 | 47 | | Home | Exchanges | Rewards | Leaderboard | 48 | |:----------|:----------|:----------|:----------| 49 | | | | | | 50 | | | | | | 51 | 52 | ## Documentation 📝 53 | -------------------------------------------------------------------------------- /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 | **/google-services.json 15 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | apply plugin: 'com.google.gms.google-services' 28 | 29 | android { 30 | compileSdkVersion flutter.compileSdkVersion 31 | ndkVersion flutter.ndkVersion 32 | 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | 38 | kotlinOptions { 39 | jvmTarget = '1.8' 40 | } 41 | 42 | sourceSets { 43 | main.java.srcDirs += 'src/main/kotlin' 44 | } 45 | 46 | defaultConfig { 47 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 48 | applicationId "com.tarekalabd.stepstracker" 49 | // You can update the following values to match your application needs. 50 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 51 | minSdkVersion 21 52 | targetSdkVersion flutter.targetSdkVersion 53 | versionCode flutterVersionCode.toInteger() 54 | versionName flutterVersionName 55 | } 56 | 57 | buildTypes { 58 | release { 59 | // TODO: Add your own signing config for the release build. 60 | // Signing with the debug keys for now, so `flutter run --release` works. 61 | signingConfig signingConfigs.debug 62 | } 63 | } 64 | } 65 | 66 | flutter { 67 | source '../..' 68 | } 69 | 70 | dependencies { 71 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 72 | // Import the Firebase BoM 73 | implementation platform('com.google.firebase:firebase-bom:30.1.0') 74 | 75 | 76 | // Add the dependency for the Firebase SDK for Google Analytics 77 | // When using the BoM, don't specify versions in Firebase dependencies 78 | implementation 'com.google.firebase:firebase-analytics' 79 | } 80 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 16 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/tarekalabd/flutter_steps_tracker/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tarekalabd.flutter_steps_tracker 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/tarekalabd/stepstracker/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tarekalabd.stepstracker 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath 'com.google.gms:google-services:4.3.10' 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | mavenCentral() 19 | } 20 | } 21 | 22 | rootProject.buildDir = '../build' 23 | subprojects { 24 | project.buildDir = "${rootProject.buildDir}/${project.name}" 25 | } 26 | subprojects { 27 | project.evaluationDependsOn(':app') 28 | } 29 | 30 | task clean(type: Delete) { 31 | delete rootProject.buildDir 32 | } 33 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 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-7.4-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/images/man-intro.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/assets/images/man-intro.jpeg -------------------------------------------------------------------------------- /assets/images/rewards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/assets/images/rewards.png -------------------------------------------------------------------------------- /assets/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/assets/screenshots/1.png -------------------------------------------------------------------------------- /assets/screenshots/2-1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/assets/screenshots/2-1.PNG -------------------------------------------------------------------------------- /assets/screenshots/2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/assets/screenshots/2.PNG -------------------------------------------------------------------------------- /assets/screenshots/3-1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/assets/screenshots/3-1.PNG -------------------------------------------------------------------------------- /assets/screenshots/3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/assets/screenshots/3.PNG -------------------------------------------------------------------------------- /assets/screenshots/4-1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/assets/screenshots/4-1.PNG -------------------------------------------------------------------------------- /assets/screenshots/4.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/assets/screenshots/4.PNG -------------------------------------------------------------------------------- /assets/screenshots/5-1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/assets/screenshots/5-1.PNG -------------------------------------------------------------------------------- /assets/screenshots/5.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/assets/screenshots/5.PNG -------------------------------------------------------------------------------- /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 | Runner/GoogleService-Info.plist 30 | 31 | # Exceptions to above rules. 32 | !default.mode1v3 33 | !default.mode2v3 34 | !default.pbxuser 35 | !default.perspectivev3 36 | -------------------------------------------------------------------------------- /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 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /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/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TarekAlabd/Flutter-Steps-Tracker/4e9156419bdef84964231b56fc5e283808ee53e9/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Flutter Steps Tracker 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | flutter_steps_tracker 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | NSMotionUsageDescription 30 | This application tracks your steps 31 | UIBackgroundModes 32 | 33 | processing 34 | 35 | UILaunchStoryboardName 36 | LaunchScreen 37 | UIMainStoryboardFile 38 | Main 39 | UISupportedInterfaceOrientations 40 | 41 | UIInterfaceOrientationPortrait 42 | 43 | UISupportedInterfaceOrientations~ipad 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationPortraitUpsideDown 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UIViewControllerBasedStatusBarAppearance 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/core/data/data_sources/cache_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:injectable/injectable.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | 6 | abstract class CacheHelper { 7 | Future get(String key); 8 | 9 | Future has(String key); 10 | 11 | Future put(String key, dynamic value); 12 | 13 | Future clear(String key); 14 | } 15 | 16 | @Singleton(as: CacheHelper) 17 | class CacheHelperImpl extends CacheHelper { 18 | final SharedPreferences _sharedPreferences; 19 | 20 | CacheHelperImpl(this._sharedPreferences); 21 | 22 | @override 23 | Future has(String key) async { 24 | final bool f = await _basicErrorHandling(() async { 25 | return _sharedPreferences.containsKey(key) && 26 | _sharedPreferences.getString(key) != null && 27 | _sharedPreferences.getString(key)!.isNotEmpty; 28 | }); 29 | return f; 30 | } 31 | 32 | @override 33 | Future clear(String key) async { 34 | final bool f = await _basicErrorHandling(() async { 35 | return await _sharedPreferences.remove(key); 36 | }); 37 | return f; 38 | } 39 | 40 | @override 41 | Future get(String key) async { 42 | final f = await _basicErrorHandling(() async { 43 | if (await has(key)) { 44 | return await jsonDecode(_sharedPreferences.getString(key)!); 45 | } 46 | return null; 47 | }); 48 | return f; 49 | } 50 | 51 | @override 52 | Future put(String key, dynamic value) async { 53 | final bool f = await _basicErrorHandling(() async { 54 | // ignore: unnecessary_await_in_return 55 | return await _sharedPreferences.setString(key, jsonEncode(value)); 56 | }); 57 | return f; 58 | } 59 | } 60 | 61 | extension on CacheHelper { 62 | Future _basicErrorHandling(Future Function() onSuccess) async { 63 | try { 64 | final f = await onSuccess(); 65 | return f; 66 | } catch (e) { 67 | rethrow; 68 | // throw ClientException.cacheError(message: e.toString()); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/core/data/data_sources/data_sources_body.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:firebase_core/firebase_core.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter_steps_tracker/core/data/error/exceptions/application_exception.dart'; 6 | import 'package:logging/logging.dart'; 7 | 8 | Future returnOrThrow(Future Function() fun) async { 9 | try { 10 | return await fun(); 11 | } on FirebaseException catch (e) { 12 | firebaseErrorDecoder(e); 13 | } on Exception catch (e, s) { 14 | if (kDebugMode) { 15 | log("DataSourceError:\n $e", level: Level.SEVERE.value, stackTrace: s); 16 | } 17 | throw GenericApplicationException(message: 'Something went wrong!'); 18 | } catch (e, s) { 19 | if (kDebugMode) { 20 | log("DataSourceError:\n $e", level: Level.SEVERE.value, stackTrace: s); 21 | } 22 | throw GenericApplicationException(message: 'Something went wrong!'); 23 | } 24 | throw GenericApplicationException(message: 'Something went wrong!'); 25 | } 26 | -------------------------------------------------------------------------------- /lib/core/data/data_sources/database.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_steps_tracker/core/data/models/steps_and_points_model.dart'; 2 | import 'package:flutter_steps_tracker/core/data/models/user_model.dart'; 3 | import 'package:flutter_steps_tracker/core/data/services/firestore_services.dart'; 4 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/exchange_history_model.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/reward_model.dart'; 6 | import 'package:flutter_steps_tracker/utilities/constants/api_path.dart'; 7 | import 'package:injectable/injectable.dart'; 8 | import 'package:intl/intl.dart'; 9 | 10 | abstract class Database { 11 | Future setUserData(UserModel user); 12 | 13 | Future setExchangeHistory( 14 | ExchangeHistoryModel history, 15 | String uid, 16 | ); 17 | 18 | Future setDailySteps( 19 | StepsAndPointsModel stepsAndPoints, 20 | String uid, 21 | ); 22 | 23 | Future setRewardOrder( 24 | RewardModel reward, 25 | String uid, 26 | ); 27 | 28 | Stream getUserStream(String uid); 29 | 30 | Stream> rewardsStream(); 31 | 32 | Stream> usersStream(); 33 | 34 | Stream> myRewardsStream(String uid); 35 | 36 | Stream> dailyPointsStream( 37 | String uid, 38 | String currentId, 39 | ); 40 | 41 | Stream> exchangeHistoryStream(String uid); 42 | } 43 | 44 | String documentIdFromLocalGenerator() => DateTime.now().toIso8601String(); 45 | 46 | String documentIdForDailyUse() => DateFormat.yMMMMd().format(DateTime.now()); 47 | 48 | @Singleton(as: Database) 49 | class FireStoreDatabase implements Database { 50 | final _service = FirestoreService.instance; 51 | 52 | @override 53 | Future setUserData(UserModel user) async { 54 | await _service.setData( 55 | path: APIPath.user(user.uid), 56 | data: user.toMap(), 57 | ); 58 | } 59 | 60 | @override 61 | Future setExchangeHistory( 62 | ExchangeHistoryModel history, 63 | String uid, 64 | ) async { 65 | await _service.setData( 66 | path: APIPath.exchangeHistory(uid, history.id), 67 | data: history.toMap(), 68 | ); 69 | } 70 | 71 | @override 72 | Stream> rewardsStream() => _service.collectionStream( 73 | path: APIPath.rewards(), 74 | builder: (data, documentId) => RewardModel.fromMap(data, documentId), 75 | ); 76 | 77 | @override 78 | Stream> exchangeHistoryStream(String uid) => 79 | _service.collectionStream( 80 | path: APIPath.exchangesHistory(uid), 81 | builder: (data, documentId) => 82 | ExchangeHistoryModel.fromMap(data, documentId), 83 | ); 84 | 85 | @override 86 | Stream> dailyPointsStream( 87 | String uid, 88 | String currentId, 89 | ) => 90 | _service.collectionStream( 91 | path: APIPath.dailyStepsAndPointsStream(uid), 92 | builder: (data, documentId) => 93 | StepsAndPointsModel.fromMap(data, documentId), 94 | queryBuilder: (query) => query.where( 95 | 'id', 96 | isNotEqualTo: currentId, 97 | ), 98 | ); 99 | 100 | @override 101 | Future setDailySteps( 102 | StepsAndPointsModel stepsAndPoints, String uid) async => 103 | _service.setData( 104 | path: APIPath.setDailyStepsAndPoints(uid, stepsAndPoints.id), 105 | data: stepsAndPoints.toMap(), 106 | ); 107 | 108 | @override 109 | Future setRewardOrder(RewardModel reward, String uid) async => 110 | _service.setData( 111 | path: APIPath.setMyReward(uid, reward.id), 112 | data: reward.toMap(), 113 | ); 114 | 115 | @override 116 | Stream> myRewardsStream(String uid) => 117 | _service.collectionStream( 118 | path: APIPath.myRewards(uid), 119 | builder: (data, documentId) => RewardModel.fromMap(data, documentId), 120 | ); 121 | 122 | @override 123 | Stream> usersStream() => _service.collectionStream( 124 | path: APIPath.users(), 125 | builder: (data, documentId) => UserModel.fromMap(data, documentId), 126 | ); 127 | 128 | @override 129 | Stream getUserStream(String uid) => _service.documentStream( 130 | path: APIPath.user(uid), 131 | builder: (data, documentId) => UserModel.fromMap(data, documentId), 132 | ); 133 | } 134 | -------------------------------------------------------------------------------- /lib/core/data/error/exceptions/application_exception.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter_steps_tracker/core/data/error/exceptions/firebase_auth_exception_app.dart'; 4 | 5 | abstract class ApplicationException implements Exception {} 6 | 7 | class GenericApplicationException extends ApplicationException { 8 | final String message; 9 | 10 | GenericApplicationException({required this.message}); 11 | } 12 | 13 | void firebaseErrorDecoder(FirebaseException e) { 14 | debugPrint(e.toString()); 15 | if (e is FirebaseAuthException) { 16 | decodeAuthException(e); 17 | } else { 18 | // TODO: Now it will be a general exception but after that, 19 | // we can detect the other firebase exceptions, client errors.. 20 | throw GenericApplicationException(message: 'Something went wrong!'); 21 | } 22 | } 23 | 24 | void decodeAuthException(FirebaseAuthException e) { 25 | // We need just the anonymous one for now, but for more 26 | // we will create enum with the types 27 | if (e.code == 'auth/operation-not-allowed') { 28 | throw FirebaseAuthExceptionApp.operationNotAllowed( 29 | message: 'Something went wrong, please contact the support!', 30 | ); 31 | } else { 32 | throw GenericApplicationException(message: 'Something went wrong!'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/core/data/error/exceptions/firebase_auth_exception_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_steps_tracker/core/data/error/exceptions/application_exception.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'firebase_auth_exception_app.freezed.dart'; 5 | 6 | @freezed 7 | @Implements() 8 | abstract class FirebaseAuthExceptionApp extends ApplicationException 9 | with _$FirebaseAuthExceptionApp { 10 | factory FirebaseAuthExceptionApp.operationNotAllowed( 11 | {required String message}) = OperationNotAllowed; 12 | } 13 | -------------------------------------------------------------------------------- /lib/core/data/error/failures/application_failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_steps_tracker/core/data/error/exceptions/application_exception.dart'; 2 | import 'package:flutter_steps_tracker/core/data/error/exceptions/firebase_auth_exception_app.dart'; 3 | import 'package:flutter_steps_tracker/core/data/error/failures/firebase_auth_failure.dart'; 4 | 5 | abstract class Failure {} 6 | 7 | /// Can be used when the list of exceptions is exhausted. 8 | /// Also, It maps to [GenericApplicationException]. 9 | class GenericFailure extends Failure { 10 | final String message; 11 | 12 | GenericFailure({required this.message}); 13 | } 14 | 15 | Failure firebaseExceptionsDecoder(ApplicationException e) { 16 | if (e is FirebaseAuthExceptionApp) { 17 | return e.when( 18 | operationNotAllowed: (msg) => 19 | FirebaseAuthFailure.operationNotAllowed(message: msg), 20 | ); 21 | } else { 22 | return GenericFailure(message: 'Something went wrong!'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/core/data/error/failures/firebase_auth_failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_steps_tracker/core/data/error/failures/application_failure.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'firebase_auth_failure.freezed.dart'; 5 | 6 | @freezed 7 | @Implements() 8 | abstract class FirebaseAuthFailure extends Failure with _$FirebaseAuthFailure { 9 | const factory FirebaseAuthFailure.operationNotAllowed( 10 | {required String message}) = OperationNotAllowed; 11 | } 12 | -------------------------------------------------------------------------------- /lib/core/data/error/failures/firebase_auth_failure.freezed.dart: -------------------------------------------------------------------------------- 1 | // coverage:ignore-file 2 | // GENERATED CODE - DO NOT MODIFY BY HAND 3 | // ignore_for_file: type=lint 4 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target 5 | 6 | part of 'firebase_auth_failure.dart'; 7 | 8 | // ************************************************************************** 9 | // FreezedGenerator 10 | // ************************************************************************** 11 | 12 | T _$identity(T value) => value; 13 | 14 | final _privateConstructorUsedError = UnsupportedError( 15 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); 16 | 17 | /// @nodoc 18 | mixin _$FirebaseAuthFailure { 19 | String get message => throw _privateConstructorUsedError; 20 | @optionalTypeArgs 21 | TResult when({ 22 | required TResult Function(String message) operationNotAllowed, 23 | }) => 24 | throw _privateConstructorUsedError; 25 | @optionalTypeArgs 26 | TResult? whenOrNull({ 27 | TResult Function(String message)? operationNotAllowed, 28 | }) => 29 | throw _privateConstructorUsedError; 30 | @optionalTypeArgs 31 | TResult maybeWhen({ 32 | TResult Function(String message)? operationNotAllowed, 33 | required TResult orElse(), 34 | }) => 35 | throw _privateConstructorUsedError; 36 | @optionalTypeArgs 37 | TResult map({ 38 | required TResult Function(OperationNotAllowed value) operationNotAllowed, 39 | }) => 40 | throw _privateConstructorUsedError; 41 | @optionalTypeArgs 42 | TResult? mapOrNull({ 43 | TResult Function(OperationNotAllowed value)? operationNotAllowed, 44 | }) => 45 | throw _privateConstructorUsedError; 46 | @optionalTypeArgs 47 | TResult maybeMap({ 48 | TResult Function(OperationNotAllowed value)? operationNotAllowed, 49 | required TResult orElse(), 50 | }) => 51 | throw _privateConstructorUsedError; 52 | 53 | @JsonKey(ignore: true) 54 | $FirebaseAuthFailureCopyWith get copyWith => 55 | throw _privateConstructorUsedError; 56 | } 57 | 58 | /// @nodoc 59 | abstract class $FirebaseAuthFailureCopyWith<$Res> { 60 | factory $FirebaseAuthFailureCopyWith( 61 | FirebaseAuthFailure value, $Res Function(FirebaseAuthFailure) then) = 62 | _$FirebaseAuthFailureCopyWithImpl<$Res>; 63 | $Res call({String message}); 64 | } 65 | 66 | /// @nodoc 67 | class _$FirebaseAuthFailureCopyWithImpl<$Res> 68 | implements $FirebaseAuthFailureCopyWith<$Res> { 69 | _$FirebaseAuthFailureCopyWithImpl(this._value, this._then); 70 | 71 | final FirebaseAuthFailure _value; 72 | // ignore: unused_field 73 | final $Res Function(FirebaseAuthFailure) _then; 74 | 75 | @override 76 | $Res call({ 77 | Object? message = freezed, 78 | }) { 79 | return _then(_value.copyWith( 80 | message: message == freezed 81 | ? _value.message 82 | : message // ignore: cast_nullable_to_non_nullable 83 | as String, 84 | )); 85 | } 86 | } 87 | 88 | /// @nodoc 89 | abstract class _$$OperationNotAllowedCopyWith<$Res> 90 | implements $FirebaseAuthFailureCopyWith<$Res> { 91 | factory _$$OperationNotAllowedCopyWith(_$OperationNotAllowed value, 92 | $Res Function(_$OperationNotAllowed) then) = 93 | __$$OperationNotAllowedCopyWithImpl<$Res>; 94 | @override 95 | $Res call({String message}); 96 | } 97 | 98 | /// @nodoc 99 | class __$$OperationNotAllowedCopyWithImpl<$Res> 100 | extends _$FirebaseAuthFailureCopyWithImpl<$Res> 101 | implements _$$OperationNotAllowedCopyWith<$Res> { 102 | __$$OperationNotAllowedCopyWithImpl( 103 | _$OperationNotAllowed _value, $Res Function(_$OperationNotAllowed) _then) 104 | : super(_value, (v) => _then(v as _$OperationNotAllowed)); 105 | 106 | @override 107 | _$OperationNotAllowed get _value => super._value as _$OperationNotAllowed; 108 | 109 | @override 110 | $Res call({ 111 | Object? message = freezed, 112 | }) { 113 | return _then(_$OperationNotAllowed( 114 | message: message == freezed 115 | ? _value.message 116 | : message // ignore: cast_nullable_to_non_nullable 117 | as String, 118 | )); 119 | } 120 | } 121 | 122 | /// @nodoc 123 | 124 | class _$OperationNotAllowed implements OperationNotAllowed { 125 | const _$OperationNotAllowed({required this.message}); 126 | 127 | @override 128 | final String message; 129 | 130 | @override 131 | String toString() { 132 | return 'FirebaseAuthFailure.operationNotAllowed(message: $message)'; 133 | } 134 | 135 | @override 136 | bool operator ==(dynamic other) { 137 | return identical(this, other) || 138 | (other.runtimeType == runtimeType && 139 | other is _$OperationNotAllowed && 140 | const DeepCollectionEquality().equals(other.message, message)); 141 | } 142 | 143 | @override 144 | int get hashCode => 145 | Object.hash(runtimeType, const DeepCollectionEquality().hash(message)); 146 | 147 | @JsonKey(ignore: true) 148 | @override 149 | _$$OperationNotAllowedCopyWith<_$OperationNotAllowed> get copyWith => 150 | __$$OperationNotAllowedCopyWithImpl<_$OperationNotAllowed>( 151 | this, _$identity); 152 | 153 | @override 154 | @optionalTypeArgs 155 | TResult when({ 156 | required TResult Function(String message) operationNotAllowed, 157 | }) { 158 | return operationNotAllowed(message); 159 | } 160 | 161 | @override 162 | @optionalTypeArgs 163 | TResult? whenOrNull({ 164 | TResult Function(String message)? operationNotAllowed, 165 | }) { 166 | return operationNotAllowed?.call(message); 167 | } 168 | 169 | @override 170 | @optionalTypeArgs 171 | TResult maybeWhen({ 172 | TResult Function(String message)? operationNotAllowed, 173 | required TResult orElse(), 174 | }) { 175 | if (operationNotAllowed != null) { 176 | return operationNotAllowed(message); 177 | } 178 | return orElse(); 179 | } 180 | 181 | @override 182 | @optionalTypeArgs 183 | TResult map({ 184 | required TResult Function(OperationNotAllowed value) operationNotAllowed, 185 | }) { 186 | return operationNotAllowed(this); 187 | } 188 | 189 | @override 190 | @optionalTypeArgs 191 | TResult? mapOrNull({ 192 | TResult Function(OperationNotAllowed value)? operationNotAllowed, 193 | }) { 194 | return operationNotAllowed?.call(this); 195 | } 196 | 197 | @override 198 | @optionalTypeArgs 199 | TResult maybeMap({ 200 | TResult Function(OperationNotAllowed value)? operationNotAllowed, 201 | required TResult orElse(), 202 | }) { 203 | if (operationNotAllowed != null) { 204 | return operationNotAllowed(this); 205 | } 206 | return orElse(); 207 | } 208 | } 209 | 210 | abstract class OperationNotAllowed implements FirebaseAuthFailure { 211 | const factory OperationNotAllowed({required final String message}) = 212 | _$OperationNotAllowed; 213 | 214 | @override 215 | String get message => throw _privateConstructorUsedError; 216 | @override 217 | @JsonKey(ignore: true) 218 | _$$OperationNotAllowedCopyWith<_$OperationNotAllowed> get copyWith => 219 | throw _privateConstructorUsedError; 220 | } 221 | -------------------------------------------------------------------------------- /lib/core/data/models/steps_and_points_model.dart: -------------------------------------------------------------------------------- 1 | class StepsAndPointsModel { 2 | final String id; 3 | final int steps; 4 | final int points; 5 | 6 | const StepsAndPointsModel({ 7 | required this.id, 8 | required this.steps, 9 | required this.points, 10 | }); 11 | 12 | Map toMap() { 13 | return { 14 | 'id': id, 15 | 'steps': steps, 16 | 'points': points, 17 | }; 18 | } 19 | 20 | factory StepsAndPointsModel.fromMap( 21 | Map map, 22 | String documentId, 23 | ) { 24 | return StepsAndPointsModel( 25 | id: documentId, 26 | steps: map['steps'] as int, 27 | points: map['points'] as int, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/core/data/models/user_model.dart: -------------------------------------------------------------------------------- 1 | class UserModel { 2 | final String uid; 3 | final String name; 4 | final int totalSteps; 5 | final int totalCalories; 6 | final int healthPoints; 7 | 8 | UserModel({ 9 | required this.uid, 10 | required this.name, 11 | this.totalSteps = 0, 12 | this.totalCalories = 0, 13 | this.healthPoints = 0, 14 | }); 15 | 16 | factory UserModel.fromMap( 17 | Map map, 18 | String documentId, 19 | ) { 20 | return UserModel( 21 | uid: documentId, 22 | name: map['name'] as String, 23 | totalSteps: map['totalSteps'] as int, 24 | totalCalories: map['totalCalories'] as int, 25 | healthPoints: map['healthPoints'] as int, 26 | ); 27 | } 28 | 29 | Map toMap() { 30 | return { 31 | 'uid': uid, 32 | 'name': name, 33 | 'totalSteps': totalSteps, 34 | 'totalCalories': totalCalories, 35 | 'healthPoints': healthPoints, 36 | }; 37 | } 38 | 39 | UserModel copyWith({ 40 | String? uid, 41 | String? name, 42 | int? totalSteps, 43 | int? totalCalories, 44 | int? healthPoints, 45 | }) { 46 | return UserModel( 47 | uid: uid ?? this.uid, 48 | name: name ?? this.name, 49 | totalSteps: totalSteps ?? this.totalSteps, 50 | totalCalories: totalCalories ?? this.totalCalories, 51 | healthPoints: healthPoints ?? this.healthPoints, 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/core/data/services/firestore_services.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | class FirestoreService { 5 | FirestoreService._(); 6 | 7 | static final instance = FirestoreService._(); 8 | 9 | Stream> collectionStream( 10 | {required String path, 11 | required T Function(Map data, String documentId) builder, 12 | Query Function(Query query)? queryBuilder, 13 | int Function(T lhs, T rhs)? sort}) { 14 | Query query = FirebaseFirestore.instance.collection(path); 15 | if (queryBuilder != null) query = queryBuilder(query); 16 | final Stream snapshots = query.snapshots(); 17 | return snapshots.map((snapshot) { 18 | final result = snapshot.docs 19 | .map( 20 | (snapshot) => builder(snapshot.data() as Map, snapshot.id), 21 | ) 22 | .where((value) => value != null) 23 | .toList(); 24 | if (sort != null) result.sort(sort); 25 | 26 | return result; 27 | }); 28 | } 29 | 30 | Future deleteData({required String path}) async { 31 | final reference = FirebaseFirestore.instance.doc(path); 32 | debugPrint('delete: $path'); 33 | await reference.delete(); 34 | } 35 | 36 | Future setData( 37 | {required String path, required Map data}) async { 38 | final reference = FirebaseFirestore.instance.doc(path); 39 | debugPrint('$path:$data'); 40 | await reference.set(data); 41 | } 42 | 43 | Stream documentStream( 44 | {required String path, 45 | required T Function(Map data, String documentId) builder}) { 46 | final DocumentReference reference = FirebaseFirestore.instance.doc(path); 47 | final Stream snapshots = reference.snapshots(); 48 | return snapshots 49 | .map((snapshot) => builder(snapshot.data() as Map, snapshot.id)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/core/domain/use_cases/use_case.dart: -------------------------------------------------------------------------------- 1 | abstract class UseCase { 2 | Type call(Params params); 3 | } 4 | 5 | class NoParams {} 6 | -------------------------------------------------------------------------------- /lib/core/presentation/pages/landing_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/pages/bottom_navbar.dart'; 4 | import 'package:flutter_steps_tracker/features/intro/presentation/manager/auth_status/auth_status_cubit.dart'; 5 | import 'package:flutter_steps_tracker/features/intro/presentation/manager/auth_status/auth_status_state.dart'; 6 | import 'package:flutter_steps_tracker/features/intro/presentation/pages/intro_page.dart'; 7 | 8 | class LandingPage extends StatelessWidget { 9 | const LandingPage({Key? key}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return BlocConsumer( 14 | bloc: BlocProvider.of(context), 15 | listener: (context, state) { 16 | if (state is Authenticated) { 17 | const BottomNavbar(); 18 | } else { 19 | const IntroPage(); 20 | } 21 | }, 22 | buildWhen: (previousState, currentState) => previousState != currentState, 23 | builder: (context, state) { 24 | return state.maybeWhen( 25 | authenticated: () => const BottomNavbar(), 26 | unAuthenticated: () => const IntroPage(), 27 | orElse: () => const SizedBox.shrink(), 28 | ); 29 | }, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/core/presentation/widgets/android_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AndroidDialog extends StatelessWidget { 4 | final String title; 5 | final Widget? contentWidget; 6 | final String? content; 7 | final String? cancelActionText; 8 | final VoidCallback? defaultAction; 9 | final String defaultActionText; 10 | final bool isLoading; 11 | 12 | const AndroidDialog({ 13 | Key? key, 14 | required this.title, 15 | this.contentWidget, 16 | this.content, 17 | this.cancelActionText, 18 | this.defaultAction, 19 | required this.defaultActionText, 20 | this.isLoading = false, 21 | }) : super(key: key); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return AlertDialog( 26 | title: Text(title), 27 | content: contentWidget == null && content != null 28 | ? Text(content!) 29 | : contentWidget, 30 | actions: [ 31 | if (cancelActionText != null) 32 | TextButton( 33 | child: Text(cancelActionText!), 34 | onPressed: () => Navigator.of(context).pop(false), 35 | ), 36 | TextButton( 37 | onPressed: defaultAction ?? () => Navigator.of(context).pop(true), 38 | child: !isLoading 39 | ? Text(defaultActionText) 40 | : const CircularProgressIndicator(), 41 | ), 42 | ], 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/core/presentation/widgets/ios_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class IosDialog extends StatelessWidget { 5 | final String title; 6 | final Widget? contentWidget; 7 | final String? content; 8 | final String? cancelActionText; 9 | final VoidCallback? defaultAction; 10 | final String defaultActionText; 11 | final bool isLoading; 12 | 13 | const IosDialog({ 14 | Key? key, 15 | required this.title, 16 | this.contentWidget, 17 | this.content, 18 | this.cancelActionText, 19 | this.defaultAction, 20 | required this.defaultActionText, 21 | this.isLoading = false, 22 | }) : super(key: key); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return CupertinoAlertDialog( 27 | title: Text(title), 28 | content: contentWidget == null && content != null 29 | ? Text(content!) 30 | : contentWidget, 31 | actions: [ 32 | if (cancelActionText != null) 33 | CupertinoDialogAction( 34 | child: Text(cancelActionText!), 35 | onPressed: () => Navigator.of(context).pop(false), 36 | ), 37 | CupertinoDialogAction( 38 | onPressed: defaultAction ?? () => Navigator.of(context).pop(true), 39 | child: !isLoading 40 | ? Text(defaultActionText) 41 | : const CircularProgressIndicator(), 42 | ), 43 | ], 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/core/presentation/widgets/show_alert_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | import 'package:flutter_steps_tracker/core/presentation/widgets/android_dialog.dart'; 7 | import 'package:flutter_steps_tracker/core/presentation/widgets/ios_dialog.dart'; 8 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/rewards/rewards_cubit.dart'; 9 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/rewards/rewards_state.dart'; 10 | 11 | /// TODO: We need to refactor this especially this cubit 12 | Future showAlertDialog( 13 | BuildContext context, { 14 | required String title, 15 | String? content, 16 | Widget? contentWidget, 17 | String? cancelActionText, 18 | required String defaultActionText, 19 | VoidCallback? defaultAction, 20 | RewardsCubit? cubit, 21 | }) { 22 | if (!Platform.isIOS) { 23 | return showDialog( 24 | context: context, 25 | builder: (context) => cubit == null 26 | ? AndroidDialog( 27 | title: title, 28 | defaultActionText: defaultActionText, 29 | content: content, 30 | contentWidget: contentWidget, 31 | cancelActionText: cancelActionText, 32 | defaultAction: defaultAction, 33 | ) 34 | : BlocConsumer( 35 | bloc: cubit, 36 | listener: (context, state) { 37 | state.maybeWhen( 38 | earnLoaded: () { 39 | Navigator.of(context).pop(); 40 | }, 41 | orElse: () {}, 42 | ); 43 | }, 44 | builder: (context, state) { 45 | return state.maybeWhen( 46 | earnLoading: () => AndroidDialog( 47 | title: title, 48 | defaultActionText: defaultActionText, 49 | content: content, 50 | contentWidget: contentWidget, 51 | cancelActionText: cancelActionText, 52 | defaultAction: defaultAction, 53 | isLoading: true, 54 | ), 55 | orElse: () => AndroidDialog( 56 | title: title, 57 | defaultActionText: defaultActionText, 58 | content: content, 59 | contentWidget: contentWidget, 60 | cancelActionText: cancelActionText, 61 | defaultAction: defaultAction, 62 | ), 63 | ); 64 | }, 65 | ), 66 | ); 67 | } 68 | return showCupertinoDialog( 69 | context: context, 70 | builder: (context) => cubit == null 71 | ? IosDialog( 72 | title: title, 73 | defaultActionText: defaultActionText, 74 | content: content, 75 | contentWidget: contentWidget, 76 | cancelActionText: cancelActionText, 77 | defaultAction: defaultAction, 78 | ) 79 | : BlocConsumer( 80 | bloc: cubit, 81 | listener: (context, state) { 82 | state.maybeWhen( 83 | earnLoaded: () { 84 | Navigator.of(context).pop(); 85 | }, 86 | orElse: () {}, 87 | ); 88 | }, 89 | builder: (context, state) { 90 | return state.maybeWhen( 91 | earnLoading: () => IosDialog( 92 | title: title, 93 | defaultActionText: defaultActionText, 94 | content: content, 95 | contentWidget: contentWidget, 96 | cancelActionText: cancelActionText, 97 | defaultAction: defaultAction, 98 | isLoading: true, 99 | ), 100 | orElse: () => IosDialog( 101 | title: title, 102 | defaultActionText: defaultActionText, 103 | content: content, 104 | contentWidget: contentWidget, 105 | cancelActionText: cancelActionText, 106 | defaultAction: defaultAction, 107 | ), 108 | ); 109 | }, 110 | ), 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /lib/core/presentation/widgets/show_exception_alert_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_core/firebase_core.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_steps_tracker/core/presentation/widgets/show_alert_dialog.dart'; 4 | 5 | Future showExceptionAlertDialog( 6 | BuildContext context, { 7 | required String title, 8 | required Exception exception, 9 | }) => 10 | showAlertDialog( 11 | context, 12 | title: title, 13 | content: _message(exception), 14 | defaultActionText: 'OK', 15 | ); 16 | 17 | String _message(Exception exception) { 18 | if (exception is FirebaseException) { 19 | return exception.message!; 20 | } 21 | return exception.toString(); 22 | } 23 | -------------------------------------------------------------------------------- /lib/di/app_module.dart: -------------------------------------------------------------------------------- 1 | import 'package:injectable/injectable.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | @module 5 | abstract class AppModule { 6 | @preResolve 7 | Future get prefs => SharedPreferences.getInstance(); 8 | } 9 | -------------------------------------------------------------------------------- /lib/di/injection_container.config.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ************************************************************************** 4 | // InjectableConfigGenerator 5 | // ************************************************************************** 6 | 7 | import 'package:get_it/get_it.dart' as _i1; 8 | import 'package:injectable/injectable.dart' as _i2; 9 | import 'package:shared_preferences/shared_preferences.dart' as _i6; 10 | 11 | import '../core/data/data_sources/cache_helper.dart' as _i14; 12 | import '../core/data/data_sources/database.dart' as _i5; 13 | import '../features/bottom_navbar/data/repositories/bottom_navbar_repository_impl.dart' 14 | as _i13; 15 | import '../features/bottom_navbar/domain/repositories/bottom_navbar_repository.dart' 16 | as _i12; 17 | import '../features/bottom_navbar/domain/use_cases/earn_reward_use_case.dart' 18 | as _i15; 19 | import '../features/bottom_navbar/domain/use_cases/get_exchanges_history_use_case.dart' 20 | as _i16; 21 | import '../features/bottom_navbar/domain/use_cases/get_rewards_use_case.dart' 22 | as _i17; 23 | import '../features/bottom_navbar/domain/use_cases/get_user_data_use_case.dart' 24 | as _i18; 25 | import '../features/bottom_navbar/domain/use_cases/get_users_use_case.dart' 26 | as _i19; 27 | import '../features/bottom_navbar/domain/use_cases/set_exchange_history_use_case.dart' 28 | as _i22; 29 | import '../features/bottom_navbar/domain/use_cases/set_steps_and_points_use_case.dart' 30 | as _i23; 31 | import '../features/bottom_navbar/presentation/manager/exchanges_history/exchanges_history_cubit.dart' 32 | as _i27; 33 | import '../features/bottom_navbar/presentation/manager/home/home_cubit.dart' 34 | as _i28; 35 | import '../features/bottom_navbar/presentation/manager/leaderboard/leaderboard_cubit.dart' 36 | as _i20; 37 | import '../features/bottom_navbar/presentation/manager/rewards/rewards_cubit.dart' 38 | as _i21; 39 | import '../features/intro/data/data_sources/auth_local_data_source.dart' as _i8; 40 | import '../features/intro/data/data_sources/auth_remote_data_source.dart' 41 | as _i4; 42 | import '../features/intro/data/repositories/auth_repository_impl.dart' as _i10; 43 | import '../features/intro/data/services/auth_services.dart' as _i3; 44 | import '../features/intro/domain/repositories/auth_repository.dart' as _i9; 45 | import '../features/intro/domain/use_cases/auth_status_use_case.dart' as _i11; 46 | import '../features/intro/domain/use_cases/sign_in_anonymously_use_case.dart' 47 | as _i24; 48 | import '../features/intro/presentation/manager/auth_actions/auth_cubit.dart' 49 | as _i25; 50 | import '../features/intro/presentation/manager/auth_status/auth_status_cubit.dart' 51 | as _i26; 52 | import '../utilities/locale/cubit/utility_cubit.dart' as _i7; 53 | import 'app_module.dart' as _i29; // ignore_for_file: unnecessary_lambdas 54 | 55 | // ignore_for_file: lines_longer_than_80_chars 56 | /// initializes the registration of provided dependencies inside of [GetIt] 57 | Future<_i1.GetIt> $initGetIt(_i1.GetIt get, 58 | {String? environment, _i2.EnvironmentFilter? environmentFilter}) async { 59 | final gh = _i2.GetItHelper(get, environment, environmentFilter); 60 | final appModule = _$AppModule(); 61 | gh.singleton<_i3.AuthBase>(_i3.Auth()); 62 | gh.singleton<_i4.AuthRemoteDataSource>( 63 | _i4.AuthRemoteDataSourceImpl(authBase: get<_i3.AuthBase>())); 64 | gh.singleton<_i5.Database>(_i5.FireStoreDatabase()); 65 | await gh.factoryAsync<_i6.SharedPreferences>(() => appModule.prefs, 66 | preResolve: true); 67 | gh.factory<_i7.UtilityCubit>(() => _i7.UtilityCubit()); 68 | gh.singleton<_i8.AuthLocalDataSource>( 69 | _i8.AuthLocalDataSourceImpl(get<_i6.SharedPreferences>())); 70 | gh.singleton<_i9.AuthRepository>(_i10.AuthRepositoryImpl( 71 | get<_i4.AuthRemoteDataSource>(), 72 | get<_i8.AuthLocalDataSource>(), 73 | get<_i5.Database>())); 74 | gh.factory<_i11.AuthStatusUseCase>( 75 | () => _i11.AuthStatusUseCase(get<_i9.AuthRepository>())); 76 | gh.singleton<_i12.BottomNavbarRepository>(_i13.BottomNavbarRepositoryImpl( 77 | get<_i5.Database>(), get<_i8.AuthLocalDataSource>())); 78 | gh.singleton<_i14.CacheHelper>( 79 | _i14.CacheHelperImpl(get<_i6.SharedPreferences>())); 80 | gh.factory<_i15.EarnARewardUseCase>( 81 | () => _i15.EarnARewardUseCase(get<_i12.BottomNavbarRepository>())); 82 | gh.factory<_i16.GetHistoryExchangesUseCase>(() => 83 | _i16.GetHistoryExchangesUseCase(get<_i12.BottomNavbarRepository>())); 84 | gh.factory<_i17.GetRewardsUseCase>( 85 | () => _i17.GetRewardsUseCase(get<_i12.BottomNavbarRepository>())); 86 | gh.factory<_i18.GetUserDataUseCase>( 87 | () => _i18.GetUserDataUseCase(get<_i12.BottomNavbarRepository>())); 88 | gh.factory<_i19.GetUsersUseCase>( 89 | () => _i19.GetUsersUseCase(get<_i12.BottomNavbarRepository>())); 90 | gh.factory<_i20.LeaderboardCubit>( 91 | () => _i20.LeaderboardCubit(get<_i19.GetUsersUseCase>())); 92 | gh.factory<_i21.RewardsCubit>(() => _i21.RewardsCubit( 93 | get<_i17.GetRewardsUseCase>(), 94 | get<_i18.GetUserDataUseCase>(), 95 | get<_i15.EarnARewardUseCase>())); 96 | gh.factory<_i22.SetExchangeHistoryUseCase>( 97 | () => _i22.SetExchangeHistoryUseCase(get<_i12.BottomNavbarRepository>())); 98 | gh.factory<_i23.SetStepsAndPointsUseCase>( 99 | () => _i23.SetStepsAndPointsUseCase(get<_i12.BottomNavbarRepository>())); 100 | gh.factory<_i24.SignInAnonymouslyUseCase>(() => 101 | _i24.SignInAnonymouslyUseCase(authRepository: get<_i9.AuthRepository>())); 102 | gh.singleton<_i25.AuthCubit>( 103 | _i25.AuthCubit(get<_i24.SignInAnonymouslyUseCase>())); 104 | gh.singleton<_i26.AuthStatusCubit>( 105 | _i26.AuthStatusCubit(get<_i11.AuthStatusUseCase>())); 106 | gh.factory<_i27.ExchangesHistoryCubit>( 107 | () => _i27.ExchangesHistoryCubit(get<_i16.GetHistoryExchangesUseCase>())); 108 | gh.factory<_i28.HomeCubit>(() => _i28.HomeCubit( 109 | get<_i22.SetExchangeHistoryUseCase>(), 110 | get<_i23.SetStepsAndPointsUseCase>(), 111 | get<_i18.GetUserDataUseCase>())); 112 | return get; 113 | } 114 | 115 | class _$AppModule extends _i29.AppModule {} 116 | -------------------------------------------------------------------------------- /lib/di/injection_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:get_it/get_it.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | import 'injection_container.config.dart'; 5 | 6 | GetIt getIt = GetIt.instance; 7 | 8 | @injectableInit 9 | Future configure() async => $initGetIt(getIt); 10 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/data/mappers/user_model_to_entity_mapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_steps_tracker/core/data/models/user_model.dart'; 2 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/entities/leaderboard_item_entity.dart'; 3 | 4 | extension UserModelToMapper on List { 5 | List toEntity() => map((e) => LeaderboardItemEntity( 6 | uid: e.uid, 7 | name: e.name, 8 | stepsNumber: e.totalSteps, 9 | order: 0, 10 | healthPoints: e.healthPoints, 11 | )).toList(); 12 | } 13 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/data/models/exchange_history_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | // TODO: We will use this model as an entity too just for simplicity now 4 | class ExchangeHistoryModel extends Equatable { 5 | final String id; 6 | final String title; 7 | final String date; 8 | final int points; 9 | 10 | const ExchangeHistoryModel({ 11 | required this.id, 12 | required this.title, 13 | required this.date, 14 | required this.points, 15 | }); 16 | 17 | Map toMap() { 18 | return { 19 | 'id': id, 20 | 'title': title, 21 | 'date': date, 22 | 'points': points, 23 | }; 24 | } 25 | 26 | factory ExchangeHistoryModel.fromMap( 27 | Map map, 28 | String documentId, 29 | ) { 30 | return ExchangeHistoryModel( 31 | id: documentId, 32 | title: map['title'] as String, 33 | date: map['date'] as String, 34 | points: map['points'] as int, 35 | ); 36 | } 37 | 38 | @override 39 | List get props => [id]; 40 | } 41 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/data/models/reward_model.dart: -------------------------------------------------------------------------------- 1 | // TODO: We will use this model as an entity too just for simplicity now 2 | import 'package:equatable/equatable.dart'; 3 | 4 | class RewardModel extends Equatable { 5 | final String id; 6 | final String name; 7 | final String description; 8 | final String imageUrl; 9 | final int points; 10 | final String qrCode; 11 | 12 | const RewardModel({ 13 | required this.id, 14 | required this.name, 15 | required this.description, 16 | required this.imageUrl, 17 | required this.points, 18 | required this.qrCode, 19 | }); 20 | 21 | Map toMap() { 22 | return { 23 | 'id': id, 24 | 'name': name, 25 | 'description': description, 26 | 'imageUrl': imageUrl, 27 | 'points': points, 28 | 'qrCode': qrCode, 29 | }; 30 | } 31 | 32 | factory RewardModel.fromMap(Map map, String documentId) { 33 | return RewardModel( 34 | id: documentId, 35 | name: map['name'] as String, 36 | description: map['description'] as String, 37 | imageUrl: map['imageUrl'] as String, 38 | points: map['points'] as int, 39 | qrCode: map['qrCode'] as String, 40 | ); 41 | } 42 | 43 | @override 44 | List get props => [id]; 45 | 46 | RewardModel copyWith({ 47 | String? id, 48 | String? name, 49 | String? description, 50 | String? imageUrl, 51 | int? points, 52 | String? qrCode, 53 | }) { 54 | return RewardModel( 55 | id: id ?? this.id, 56 | name: name ?? this.name, 57 | description: description ?? this.description, 58 | imageUrl: imageUrl ?? this.imageUrl, 59 | points: points ?? this.points, 60 | qrCode: qrCode ?? this.qrCode, 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/data/repositories/bottom_navbar_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dartz/dartz.dart'; 4 | import 'package:flutter_steps_tracker/core/data/data_sources/database.dart'; 5 | import 'package:flutter_steps_tracker/core/data/error/exceptions/application_exception.dart'; 6 | import 'package:flutter_steps_tracker/core/data/error/failures/application_failure.dart'; 7 | import 'package:flutter_steps_tracker/core/data/models/steps_and_points_model.dart'; 8 | import 'package:flutter_steps_tracker/core/data/models/user_model.dart'; 9 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/exchange_history_model.dart'; 10 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/reward_model.dart'; 11 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/repositories/bottom_navbar_repository.dart'; 12 | import 'package:flutter_steps_tracker/features/intro/data/data_sources/auth_local_data_source.dart'; 13 | import 'package:injectable/injectable.dart'; 14 | 15 | @Singleton(as: BottomNavbarRepository) 16 | class BottomNavbarRepositoryImpl implements BottomNavbarRepository { 17 | final Database _database; 18 | final AuthLocalDataSource _authLocalDataSource; 19 | 20 | BottomNavbarRepositoryImpl( 21 | this._database, 22 | this._authLocalDataSource, 23 | ); 24 | 25 | @override 26 | Stream> rewardsStream() { 27 | return _database.rewardsStream(); 28 | } 29 | 30 | @override 31 | Future> setExchangeHistory( 32 | ExchangeHistoryModel exchangeHistory) async { 33 | try { 34 | final user = await _authLocalDataSource.currentUser(); 35 | await _database.setExchangeHistory( 36 | exchangeHistory, 37 | user!.uid, 38 | ); 39 | return const Right(true); 40 | } on ApplicationException catch (e) { 41 | return Left( 42 | firebaseExceptionsDecoder(e), 43 | ); 44 | } 45 | } 46 | 47 | @override 48 | Future>>> 49 | exchangesHistoryStream() async { 50 | try { 51 | final user = await _authLocalDataSource.currentUser(); 52 | return Right(_database.exchangeHistoryStream(user!.uid)); 53 | } on ApplicationException catch (e) { 54 | return Left( 55 | firebaseExceptionsDecoder(e), 56 | ); 57 | } 58 | } 59 | 60 | @override 61 | Future> setStepsAndPoints(int steps) async { 62 | try { 63 | final user = await _authLocalDataSource.currentUser(); 64 | var totalHealthPoints = 0; 65 | int healthPoints = (steps ~/ 100) * 5; 66 | await _database.setDailySteps( 67 | StepsAndPointsModel( 68 | id: documentIdForDailyUse(), 69 | steps: steps, 70 | points: healthPoints, 71 | ), 72 | user!.uid, 73 | ); 74 | var myRewardsList = await _database.myRewardsStream(user.uid).first; 75 | int deletedPoints = 0; 76 | for (var reward in myRewardsList) { 77 | deletedPoints += reward.points; 78 | } 79 | totalHealthPoints = healthPoints - deletedPoints; 80 | final newUser = UserModel( 81 | uid: user.uid, 82 | name: user.name, 83 | totalSteps: steps, 84 | healthPoints: totalHealthPoints, 85 | ); 86 | await _database.setUserData(newUser); 87 | await _authLocalDataSource.persistAuth(newUser); 88 | return const Right(true); 89 | } on ApplicationException catch (e) { 90 | return Left( 91 | firebaseExceptionsDecoder(e), 92 | ); 93 | } 94 | } 95 | 96 | @override 97 | Future> getUserData() async { 98 | try { 99 | final user = await _authLocalDataSource.currentUser(); 100 | return Right(user!); 101 | } on ApplicationException catch (e) { 102 | return Left( 103 | firebaseExceptionsDecoder(e), 104 | ); 105 | } 106 | } 107 | 108 | @override 109 | Future>> getRealTimeUserData() async { 110 | try { 111 | final user = await _authLocalDataSource.currentUser(); 112 | return Right(_database.getUserStream(user!.uid)); 113 | } on ApplicationException catch (e) { 114 | return Left( 115 | firebaseExceptionsDecoder(e), 116 | ); 117 | } 118 | } 119 | 120 | @override 121 | Future> earnAReward(RewardModel reward) async { 122 | try { 123 | final user = await _authLocalDataSource.currentUser(); 124 | await _database.setRewardOrder( 125 | reward.copyWith( 126 | id: documentIdFromLocalGenerator(), 127 | ), 128 | user!.uid, 129 | ); 130 | var realUserData = await _database.getUserStream(user.uid).first; 131 | await _database.setUserData( 132 | realUserData.copyWith( 133 | healthPoints: realUserData.healthPoints - reward.points), 134 | ); 135 | return const Right(true); 136 | } on ApplicationException catch (e) { 137 | return Left( 138 | firebaseExceptionsDecoder(e), 139 | ); 140 | } 141 | } 142 | 143 | @override 144 | Future>>> usersStream() async { 145 | try { 146 | return Right(_database.usersStream()); 147 | } on ApplicationException catch (e) { 148 | return Left( 149 | firebaseExceptionsDecoder(e), 150 | ); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/domain/entities/leaderboard_item_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class LeaderboardItemEntity extends Equatable { 4 | final String uid; 5 | final String imageUrl; 6 | final String name; 7 | final int stepsNumber; 8 | final int order; 9 | final int healthPoints; 10 | 11 | const LeaderboardItemEntity({ 12 | required this.uid, 13 | this.imageUrl = 14 | 'https://t3.ftcdn.net/jpg/03/46/83/96/360_F_346839683_6nAPzbhpSkIpb8pmAwufkC7c5eD7wYws.jpg', 15 | required this.name, 16 | required this.stepsNumber, 17 | required this.order, 18 | required this.healthPoints, 19 | }); 20 | 21 | @override 22 | List get props => [uid]; 23 | 24 | LeaderboardItemEntity copyWith({ 25 | String? uid, 26 | String? imageUrl, 27 | String? name, 28 | int? stepsNumber, 29 | int? order, 30 | int? healthPoints, 31 | }) { 32 | return LeaderboardItemEntity( 33 | uid: uid ?? this.uid, 34 | imageUrl: imageUrl ?? this.imageUrl, 35 | name: name ?? this.name, 36 | stepsNumber: stepsNumber ?? this.stepsNumber, 37 | order: order ?? this.order, 38 | healthPoints: healthPoints ?? this.healthPoints, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/domain/repositories/bottom_navbar_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_steps_tracker/core/data/error/failures/application_failure.dart'; 3 | import 'package:flutter_steps_tracker/core/data/models/user_model.dart'; 4 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/exchange_history_model.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/reward_model.dart'; 6 | 7 | abstract class BottomNavbarRepository { 8 | Stream> rewardsStream(); 9 | 10 | Future>>> usersStream(); 11 | 12 | Future>>> 13 | exchangesHistoryStream(); 14 | 15 | Future> setExchangeHistory( 16 | ExchangeHistoryModel exchangeHistory); 17 | 18 | Future> setStepsAndPoints(int stepsParams); 19 | 20 | Future> getUserData(); 21 | 22 | Future>> getRealTimeUserData(); 23 | 24 | Future> earnAReward(RewardModel reward); 25 | } 26 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/domain/use_cases/earn_reward_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_steps_tracker/core/data/error/failures/application_failure.dart'; 3 | import 'package:flutter_steps_tracker/core/domain/use_cases/use_case.dart'; 4 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/reward_model.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/repositories/bottom_navbar_repository.dart'; 6 | import 'package:injectable/injectable.dart'; 7 | 8 | @injectable 9 | class EarnARewardUseCase 10 | extends UseCase>, RewardModel> { 11 | final BottomNavbarRepository _bottomNavbarRepository; 12 | 13 | EarnARewardUseCase(this._bottomNavbarRepository); 14 | 15 | @override 16 | Future> call(RewardModel params) async => 17 | await _bottomNavbarRepository.earnAReward(params); 18 | } 19 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/domain/use_cases/get_exchanges_history_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_steps_tracker/core/data/error/failures/application_failure.dart'; 3 | import 'package:flutter_steps_tracker/core/domain/use_cases/use_case.dart'; 4 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/exchange_history_model.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/repositories/bottom_navbar_repository.dart'; 6 | import 'package:injectable/injectable.dart'; 7 | 8 | @injectable 9 | class GetHistoryExchangesUseCase extends UseCase< 10 | Future>>>, NoParams> { 11 | final BottomNavbarRepository _bottomNavbarRepository; 12 | 13 | GetHistoryExchangesUseCase(this._bottomNavbarRepository); 14 | 15 | @override 16 | Future>>> call( 17 | NoParams params) async => 18 | await _bottomNavbarRepository.exchangesHistoryStream(); 19 | } 20 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/domain/use_cases/get_rewards_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_steps_tracker/core/domain/use_cases/use_case.dart'; 2 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/reward_model.dart'; 3 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/repositories/bottom_navbar_repository.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | 6 | @injectable 7 | class GetRewardsUseCase extends UseCase>, NoParams> { 8 | final BottomNavbarRepository _bottomNavbarRepository; 9 | 10 | GetRewardsUseCase(this._bottomNavbarRepository); 11 | 12 | @override 13 | Stream> call(NoParams params) => 14 | _bottomNavbarRepository.rewardsStream(); 15 | } 16 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/domain/use_cases/get_user_data_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_steps_tracker/core/data/error/failures/application_failure.dart'; 3 | import 'package:flutter_steps_tracker/core/data/models/user_model.dart'; 4 | import 'package:flutter_steps_tracker/core/domain/use_cases/use_case.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/repositories/bottom_navbar_repository.dart'; 6 | import 'package:injectable/injectable.dart'; 7 | 8 | @injectable 9 | class GetUserDataUseCase 10 | extends UseCase>>, NoParams> { 11 | final BottomNavbarRepository _bottomNavbarRepository; 12 | 13 | GetUserDataUseCase(this._bottomNavbarRepository); 14 | 15 | @override 16 | Future>> call(NoParams params) async => 17 | await _bottomNavbarRepository.getRealTimeUserData(); 18 | } 19 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/domain/use_cases/get_users_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_steps_tracker/core/data/error/failures/application_failure.dart'; 3 | import 'package:flutter_steps_tracker/core/data/models/user_model.dart'; 4 | import 'package:flutter_steps_tracker/core/domain/use_cases/use_case.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/repositories/bottom_navbar_repository.dart'; 6 | import 'package:injectable/injectable.dart'; 7 | 8 | @injectable 9 | class GetUsersUseCase extends UseCase< 10 | Future>>>, NoParams> { 11 | final BottomNavbarRepository _bottomNavbarRepository; 12 | 13 | GetUsersUseCase(this._bottomNavbarRepository); 14 | 15 | @override 16 | Future>>> call( 17 | NoParams params) async => 18 | await _bottomNavbarRepository.usersStream(); 19 | } 20 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/domain/use_cases/set_exchange_history_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_steps_tracker/core/data/error/failures/application_failure.dart'; 3 | import 'package:flutter_steps_tracker/core/domain/use_cases/use_case.dart'; 4 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/exchange_history_model.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/repositories/bottom_navbar_repository.dart'; 6 | import 'package:injectable/injectable.dart'; 7 | 8 | @injectable 9 | class SetExchangeHistoryUseCase 10 | extends UseCase>, ExchangeHistoryModel> { 11 | final BottomNavbarRepository _bottomNavbarRepository; 12 | 13 | SetExchangeHistoryUseCase(this._bottomNavbarRepository); 14 | 15 | @override 16 | Future> call(ExchangeHistoryModel params) async => 17 | await _bottomNavbarRepository.setExchangeHistory(params); 18 | } 19 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/domain/use_cases/set_steps_and_points_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_steps_tracker/core/data/error/failures/application_failure.dart'; 3 | import 'package:flutter_steps_tracker/core/domain/use_cases/use_case.dart'; 4 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/repositories/bottom_navbar_repository.dart'; 5 | import 'package:injectable/injectable.dart'; 6 | 7 | @injectable 8 | class SetStepsAndPointsUseCase 9 | extends UseCase>, int> { 10 | final BottomNavbarRepository _bottomNavbarRepository; 11 | 12 | SetStepsAndPointsUseCase(this._bottomNavbarRepository); 13 | 14 | @override 15 | Future> call(int params) async => 16 | await _bottomNavbarRepository.setStepsAndPoints(params); 17 | } 18 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/manager/exchanges_history/exchanges_history_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_steps_tracker/core/domain/use_cases/use_case.dart'; 4 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/exchange_history_model.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/use_cases/get_exchanges_history_use_case.dart'; 6 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/exchanges_history/exchanges_history_state.dart'; 7 | import 'package:flutter_steps_tracker/generated/l10n.dart'; 8 | import 'package:injectable/injectable.dart'; 9 | 10 | @injectable 11 | class ExchangesHistoryCubit extends Cubit { 12 | final GetHistoryExchangesUseCase _getExchangesHistoryUseCase; 13 | late Stream> _exchangesStream; 14 | 15 | ExchangesHistoryCubit( 16 | this._getExchangesHistoryUseCase, 17 | ) : super(const ExchangesHistoryState.initial()); 18 | 19 | Future getExchangesHistory() async { 20 | emit(const ExchangesHistoryState.loading()); 21 | final result = await _getExchangesHistoryUseCase(NoParams()); 22 | result.fold( 23 | (_) => emit( 24 | ExchangesHistoryState.error(message: S.current.somethingWentWrong)), 25 | (exchangesStream) { 26 | _exchangesStream = exchangesStream; 27 | _exchangesStream.listen(onExchangesReceived).onError(onExchangesError); 28 | }); 29 | } 30 | 31 | void onExchangesReceived(List exchanges) { 32 | debugPrint('Exchanges Length: ${exchanges.length}'); 33 | emit(ExchangesHistoryState.loaded(exchanges: exchanges)); 34 | } 35 | 36 | void onExchangesError(error) { 37 | debugPrint('onExchangesError: $error'); 38 | emit(ExchangesHistoryState.error(message: S.current.somethingWentWrong)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/manager/exchanges_history/exchanges_history_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/exchange_history_model.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'exchanges_history_state.freezed.dart'; 5 | 6 | @freezed 7 | abstract class ExchangesHistoryState with _$ExchangesHistoryState { 8 | const factory ExchangesHistoryState.initial() = Initial; 9 | 10 | const factory ExchangesHistoryState.loading() = Loading; 11 | 12 | const factory ExchangesHistoryState.loaded( 13 | {required List exchanges}) = Loaded; 14 | 15 | const factory ExchangesHistoryState.error({required String message}) = Error; 16 | } 17 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/manager/home/home_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_steps_tracker/core/data/data_sources/database.dart'; 4 | import 'package:flutter_steps_tracker/core/domain/use_cases/use_case.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/exchange_history_model.dart'; 6 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/use_cases/get_user_data_use_case.dart'; 7 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/use_cases/set_exchange_history_use_case.dart'; 8 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/use_cases/set_steps_and_points_use_case.dart'; 9 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/home/home_state.dart'; 10 | import 'package:flutter_steps_tracker/generated/l10n.dart'; 11 | import 'package:flutter_steps_tracker/utilities/constants/enums.dart'; 12 | import 'package:injectable/injectable.dart'; 13 | import 'package:pedometer/pedometer.dart'; 14 | 15 | @injectable 16 | class HomeCubit extends Cubit { 17 | final SetExchangeHistoryUseCase _setExchangeHistoryUseCase; 18 | final SetStepsAndPointsUseCase _setStepsAndPointsUseCase; 19 | final GetUserDataUseCase _getUserDataUseCase; 20 | late Stream _stepCountStream; 21 | String _steps = '?'; 22 | 23 | HomeCubit( 24 | this._setExchangeHistoryUseCase, 25 | this._setStepsAndPointsUseCase, 26 | this._getUserDataUseCase, 27 | ) : super( 28 | const HomeState.initial(), 29 | ); 30 | 31 | Future getUserData() async { 32 | emit(const HomeState.stepsAndPointsLoading()); 33 | final result = await _getUserDataUseCase(NoParams()); 34 | result.fold( 35 | (failure) => 36 | emit(HomeState.stepsError(message: S.current.somethingWentWrong)), 37 | (userData) => userData.listen( 38 | (event) { 39 | debugPrint("User Steps here: ${event.totalSteps}"); 40 | emit( 41 | HomeState.stepsAndPointsLoaded( 42 | steps: event.totalSteps, 43 | healthPoints: event.healthPoints, 44 | ), 45 | ); 46 | }, 47 | ), 48 | ); 49 | } 50 | 51 | void initPlatformState() { 52 | emit(const HomeState.loading()); 53 | _stepCountStream = Pedometer.stepCountStream; 54 | _stepCountStream.listen(onStepCount).onError(onStepCountError); 55 | } 56 | 57 | void onStepCount(StepCount event) async { 58 | debugPrint(event.toString()); 59 | var oldSteps = int.tryParse(_steps) ?? 0; 60 | _steps = event.steps.toString(); 61 | debugPrint("Steps in Cubit: $_steps"); 62 | emit(HomeState.loaded(steps: _steps)); 63 | await _setStepsAndPointsUseCase(event.steps); 64 | await onFeedbackState(oldSteps, event.steps); 65 | } 66 | 67 | Future onFeedbackState(int oldSteps, int newSteps) async { 68 | if ((oldSteps % 100) > (newSteps % 100)) { 69 | emit(HomeState.feedbackGain(steps: _steps)); 70 | await _setExchangeHistoryUseCase( 71 | ExchangeHistoryModel( 72 | id: documentIdFromLocalGenerator(), 73 | title: ExchangeHistoryTitle.exchange.title, 74 | date: DateTime.now().toIso8601String(), 75 | points: 5, 76 | ), 77 | ); 78 | } 79 | } 80 | 81 | void onStepCountError(error) { 82 | debugPrint('onStepCountError: $error'); 83 | _steps = '?'; 84 | emit(HomeState.error(message: _steps)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/manager/home/home_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'home_state.freezed.dart'; 4 | 5 | @freezed 6 | abstract class HomeState with _$HomeState { 7 | const factory HomeState.initial() = Initial; 8 | 9 | const factory HomeState.loading() = Loading; 10 | 11 | const factory HomeState.stepsAndPointsLoading() = StepsAndPointsLoading; 12 | 13 | const factory HomeState.stepsAndPointsLoaded({ 14 | required int steps, 15 | required int healthPoints, 16 | }) = StepsAndPointsLoaded; 17 | 18 | const factory HomeState.stepsError({required String message}) = StepsError; 19 | 20 | const factory HomeState.loaded({required String steps}) = Loaded; 21 | 22 | const factory HomeState.feedbackGain({required String steps}) = FeedbackGain; 23 | 24 | const factory HomeState.error({required String message}) = Error; 25 | } 26 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/manager/leaderboard/leaderboard_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:flutter_steps_tracker/core/domain/use_cases/use_case.dart'; 3 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/entities/leaderboard_item_entity.dart'; 4 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/use_cases/get_users_use_case.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/leaderboard/leaderboard_state.dart'; 6 | import 'package:flutter_steps_tracker/generated/l10n.dart'; 7 | import 'package:injectable/injectable.dart'; 8 | 9 | @injectable 10 | class LeaderboardCubit extends Cubit { 11 | final GetUsersUseCase _getUsersUseCase; 12 | late Stream> usersStream; 13 | 14 | LeaderboardCubit( 15 | this._getUsersUseCase, 16 | ) : super(const LeaderboardState.initial()); 17 | 18 | /// TODO: We will split the data and separate them in the cubit in the future 19 | Future getUsers() async { 20 | emit(const LeaderboardState.loading()); 21 | final result = await _getUsersUseCase(NoParams()); 22 | result.fold( 23 | (failure) => 24 | emit(LeaderboardState.error(message: S.current.somethingWentWrong)), 25 | (stream) => stream.listen((users) { 26 | users.sort((a, b) => b.totalSteps.compareTo(a.totalSteps)); 27 | emit(LeaderboardState.loaded(users: users)); 28 | }).onError( 29 | (error) => 30 | emit(LeaderboardState.error(message: S.current.somethingWentWrong)), 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/manager/leaderboard/leaderboard_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_steps_tracker/core/data/models/user_model.dart'; 2 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/entities/leaderboard_item_entity.dart'; 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | 5 | part 'leaderboard_state.freezed.dart'; 6 | 7 | @freezed 8 | abstract class LeaderboardState with _$LeaderboardState { 9 | const factory LeaderboardState.initial() = Initial; 10 | 11 | const factory LeaderboardState.loading() = Loading; 12 | 13 | const factory LeaderboardState.loaded({required List users}) = 14 | Loaded; 15 | 16 | const factory LeaderboardState.error({required String message}) = Error; 17 | } 18 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/manager/rewards/rewards_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_steps_tracker/core/domain/use_cases/use_case.dart'; 4 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/reward_model.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/use_cases/earn_reward_use_case.dart'; 6 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/use_cases/get_rewards_use_case.dart'; 7 | import 'package:flutter_steps_tracker/features/bottom_navbar/domain/use_cases/get_user_data_use_case.dart'; 8 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/rewards/rewards_state.dart'; 9 | import 'package:flutter_steps_tracker/generated/l10n.dart'; 10 | import 'package:injectable/injectable.dart'; 11 | 12 | @injectable 13 | class RewardsCubit extends Cubit { 14 | final GetRewardsUseCase _getRewardsUseCase; 15 | final GetUserDataUseCase _getUserDataUseCase; 16 | final EarnARewardUseCase _earnARewardUseCase; 17 | late Stream> _rewardsStream; 18 | 19 | RewardsCubit( 20 | this._getRewardsUseCase, 21 | this._getUserDataUseCase, 22 | this._earnARewardUseCase, 23 | ) : super( 24 | const RewardsState.initial(), 25 | ); 26 | 27 | Future getUserPoints() async { 28 | emit(const RewardsState.loading()); 29 | final result = await _getUserDataUseCase(NoParams()); 30 | result.fold( 31 | (failure) => emit( 32 | RewardsState.userDataError(message: S.current.somethingWentWrong)), 33 | (user) => user.listen( 34 | (event) => emit( 35 | RewardsState.userDataLoaded( 36 | points: event.healthPoints, 37 | ), 38 | ), 39 | ), 40 | ); 41 | } 42 | 43 | Future earnAReward(RewardModel reward) async { 44 | emit(const RewardsState.earnLoading()); 45 | final result = await _earnARewardUseCase(reward); 46 | emit( 47 | result.fold( 48 | (failure) { 49 | return RewardsState.earnError(message: S.current.somethingWentWrong); 50 | }, 51 | (user) => const RewardsState.earnLoaded(), 52 | ), 53 | ); 54 | } 55 | 56 | void getRewards() { 57 | emit(const RewardsState.loading()); 58 | _rewardsStream = _getRewardsUseCase(NoParams()); 59 | _rewardsStream.listen(onRewardsReceived).onError(onRewardsError); 60 | } 61 | 62 | void onRewardsReceived(List rewards) { 63 | debugPrint('Rewards Length: ${rewards.length}'); 64 | emit(RewardsState.loaded(rewards: rewards)); 65 | } 66 | 67 | void onRewardsError(error) { 68 | debugPrint('onRewardsError: $error'); 69 | emit(RewardsState.error(message: S.current.somethingWentWrong)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/manager/rewards/rewards_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/reward_model.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'rewards_state.freezed.dart'; 5 | 6 | @freezed 7 | abstract class RewardsState with _$RewardsState { 8 | const factory RewardsState.initial() = Initial; 9 | 10 | const factory RewardsState.loading() = Loading; 11 | 12 | const factory RewardsState.earnLoading() = EarnLoading; 13 | 14 | const factory RewardsState.userDataLoading() = UserDataLoading; 15 | 16 | const factory RewardsState.loaded({required List rewards}) = 17 | Loaded; 18 | 19 | const factory RewardsState.earnLoaded() = EarnLoaded; 20 | 21 | const factory RewardsState.userDataLoaded({required int points}) = 22 | UserDataLoaded; 23 | 24 | const factory RewardsState.error({required String message}) = Error; 25 | 26 | const factory RewardsState.earnError({required String message}) = EarnError; 27 | 28 | const factory RewardsState.userDataError({required String message}) = 29 | UserDataError; 30 | } 31 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/pages/bottom_navbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/pages/exchanges_page.dart'; 4 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/pages/home_page.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/pages/leaderboard_page.dart'; 6 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/pages/rewards_page.dart'; 7 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/widgets/app_bar_area.dart'; 8 | import 'package:flutter_steps_tracker/generated/l10n.dart'; 9 | import 'package:persistent_bottom_nav_bar_v2/persistent-tab-view.dart'; 10 | 11 | class BottomNavbar extends StatefulWidget { 12 | const BottomNavbar({Key? key}) : super(key: key); 13 | 14 | @override 15 | State createState() => _BottomNavbarState(); 16 | } 17 | 18 | class _BottomNavbarState extends State { 19 | final _bottomNavbarController = PersistentTabController(); 20 | 21 | List _buildScreens() { 22 | return [ 23 | const HomePage(), 24 | const ExchangesHistoryPage(), 25 | const RewardsPage(), 26 | const LeaderboardPage(), 27 | ]; 28 | } 29 | 30 | List _navBarsItems() { 31 | return [ 32 | PersistentBottomNavBarItem( 33 | icon: const Icon(CupertinoIcons.home), 34 | title: (S.current.home), 35 | activeColorPrimary: Theme.of(context).primaryColor, 36 | inactiveColorPrimary: CupertinoColors.systemGrey, 37 | ), 38 | PersistentBottomNavBarItem( 39 | icon: const Icon(Icons.track_changes), 40 | title: (S.current.exchanges), 41 | activeColorPrimary: Theme.of(context).primaryColor, 42 | inactiveColorPrimary: CupertinoColors.systemGrey, 43 | ), 44 | PersistentBottomNavBarItem( 45 | icon: const Icon(Icons.card_giftcard), 46 | title: (S.current.rewards), 47 | activeColorPrimary: Theme.of(context).primaryColor, 48 | inactiveColorPrimary: CupertinoColors.systemGrey, 49 | ), 50 | PersistentBottomNavBarItem( 51 | icon: const Icon(Icons.developer_board), 52 | title: (S.current.leaderboard), 53 | activeColorPrimary: Theme.of(context).primaryColor, 54 | inactiveColorPrimary: CupertinoColors.systemGrey, 55 | ), 56 | ]; 57 | } 58 | 59 | @override 60 | Widget build(BuildContext context) { 61 | final size = MediaQuery.of(context).size; 62 | return Scaffold( 63 | appBar: PreferredSize( 64 | preferredSize: Size(double.infinity, size.height * 0.06), 65 | child: const AppBarArea(), 66 | ), 67 | body: SafeArea( 68 | child: PersistentTabView( 69 | context, 70 | controller: _bottomNavbarController, 71 | screens: _buildScreens(), 72 | items: _navBarsItems(), 73 | confineInSafeArea: true, 74 | backgroundColor: Theme.of(context).scaffoldBackgroundColor, 75 | handleAndroidBackButtonPress: true, 76 | // Default is true. 77 | resizeToAvoidBottomInset: true, 78 | // This needs to be true if you want to move up the screen when keyboard appears. Default is true. 79 | stateManagement: true, 80 | // Default is true. 81 | hideNavigationBarWhenKeyboardShows: true, 82 | // Recommended to set 'resizeToAvoidBottomInset' as true while using this argument. Default is true. 83 | decoration: NavBarDecoration( 84 | borderRadius: BorderRadius.circular(10.0), 85 | colorBehindNavBar: Theme.of(context).scaffoldBackgroundColor, 86 | ), 87 | popAllScreensOnTapOfSelectedTab: true, 88 | popActionScreens: PopActionScreensType.all, 89 | itemAnimationProperties: const ItemAnimationProperties( 90 | // Navigation Bar's items animation properties. 91 | duration: Duration(milliseconds: 200), 92 | curve: Curves.ease, 93 | ), 94 | screenTransitionAnimation: const ScreenTransitionAnimation( 95 | // Screen transition animation on change of selected tab. 96 | animateTabTransition: true, 97 | curve: Curves.ease, 98 | duration: Duration(milliseconds: 200), 99 | ), 100 | navBarStyle: NavBarStyle 101 | .style1, // Choose the nav bar style with this property. 102 | ), 103 | ), 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/pages/exchanges_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_steps_tracker/di/injection_container.dart'; 4 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/exchange_history_model.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/exchanges_history/exchanges_history_cubit.dart'; 6 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/exchanges_history/exchanges_history_state.dart'; 7 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/widgets/exchanges_item.dart'; 8 | import 'package:flutter_steps_tracker/generated/l10n.dart'; 9 | 10 | class ExchangesHistoryPage extends StatelessWidget { 11 | const ExchangesHistoryPage({Key? key}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return BlocProvider( 16 | create: (context) { 17 | final cubit = getIt(); 18 | cubit.getExchangesHistory(); 19 | return cubit; 20 | }, 21 | child: Builder(builder: (context) { 22 | return BlocBuilder( 23 | bloc: BlocProvider.of(context), 24 | builder: (context, state) { 25 | return state.maybeWhen( 26 | loading: () => _buildExchangesList(context, isLoading: false), 27 | loaded: (exchanges) => _buildExchangesList( 28 | context, 29 | exchanges: exchanges, 30 | ), 31 | orElse: () => _buildExchangesList(context), 32 | ); 33 | }, 34 | ); 35 | }), 36 | ); 37 | } 38 | 39 | /// TODO: Refactor this to a separate class 40 | Widget _buildExchangesList(BuildContext context, 41 | {bool isLoading = false, List? exchanges}) { 42 | if (isLoading) { 43 | return const Center( 44 | child: CircularProgressIndicator(), 45 | ); 46 | } 47 | return exchanges != null && exchanges.isNotEmpty 48 | ? SingleChildScrollView( 49 | child: Padding( 50 | padding: 51 | const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0), 52 | child: Column( 53 | children: exchanges 54 | .map( 55 | (e) => Column( 56 | children: [ 57 | ExchangesItem( 58 | exchangeHistoryItem: e, 59 | ), 60 | const Divider(), 61 | ], 62 | ), 63 | ) 64 | .toList(), 65 | ), 66 | ), 67 | ) 68 | : Center( 69 | child: Text( 70 | S.of(context).emptyState, 71 | style: Theme.of(context).textTheme.subtitle1!.copyWith( 72 | color: Theme.of(context).primaryColor, 73 | ), 74 | ), 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/pages/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_steps_tracker/di/injection_container.dart'; 4 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/home/home_cubit.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/widgets/ad_area.dart'; 6 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/widgets/get_radial_gauge.dart'; 7 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/widgets/health_points_and_calories.dart'; 8 | 9 | class HomePage extends StatelessWidget { 10 | const HomePage({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return BlocProvider( 15 | create: (context) { 16 | final cubit = getIt(); 17 | cubit.getUserData(); 18 | return cubit; 19 | }, 20 | child: SingleChildScrollView( 21 | child: Padding( 22 | padding: const EdgeInsets.all(8.0), 23 | child: Column( 24 | crossAxisAlignment: CrossAxisAlignment.start, 25 | children: const [ 26 | SizedBox(height: 24.0), 27 | Padding( 28 | padding: EdgeInsets.symmetric(horizontal: 16.0), 29 | child: HealthPointsAndCalories(), 30 | ), 31 | SizedBox(height: 24.0), 32 | GetRadialGauge(), 33 | SizedBox(height: 16.0), 34 | Padding( 35 | padding: EdgeInsets.symmetric(horizontal: 24.0), 36 | child: AdArea(), 37 | ), 38 | ], 39 | ), 40 | ), 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/pages/leaderboard_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_steps_tracker/core/data/models/user_model.dart'; 4 | import 'package:flutter_steps_tracker/di/injection_container.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/leaderboard/leaderboard_cubit.dart'; 6 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/leaderboard/leaderboard_state.dart'; 7 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/widgets/leaderboard_item.dart'; 8 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/widgets/leaderboard_top_item.dart'; 9 | import 'package:flutter_steps_tracker/generated/l10n.dart'; 10 | 11 | class LeaderboardPage extends StatelessWidget { 12 | const LeaderboardPage({Key? key}) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return BlocProvider( 17 | create: (context) { 18 | final cubit = getIt(); 19 | cubit.getUsers(); 20 | return cubit; 21 | }, 22 | child: Builder(builder: (context) { 23 | return SafeArea( 24 | child: BlocBuilder( 25 | bloc: BlocProvider.of(context), 26 | builder: (context, state) { 27 | return state.maybeWhen( 28 | loading: () => _buildLeaderboardPage(context, isLoading: true), 29 | loaded: (users) => _buildLeaderboardPage(context, users: users), 30 | orElse: () => _buildLeaderboardPage(context), 31 | ); 32 | }, 33 | ), 34 | ); 35 | }), 36 | ); 37 | } 38 | 39 | Widget _buildLeaderboardPage(BuildContext context, 40 | {bool isLoading = false, List? users}) { 41 | if (isLoading) { 42 | return const Center( 43 | child: CircularProgressIndicator(), 44 | ); 45 | } 46 | if (users == null || users.isEmpty) { 47 | return Center( 48 | child: Text(S.of(context).emptyState), 49 | ); 50 | } 51 | 52 | /// TODO: we will refactor this part in the future 53 | List leftUsers = users.length > 3 ? users.sublist(3) : []; 54 | return SingleChildScrollView( 55 | child: Padding( 56 | padding: const EdgeInsets.symmetric( 57 | horizontal: 16.0, 58 | vertical: 8.0, 59 | ), 60 | child: Column( 61 | children: [ 62 | Stack( 63 | alignment: Alignment.topCenter, 64 | children: [ 65 | Padding( 66 | padding: const EdgeInsets.only(top: 48.0), 67 | child: Row( 68 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 69 | children: [ 70 | if (users.length >= 2) 71 | LeaderboardTopItem( 72 | sNumber: 2, 73 | item: users[1], 74 | ), 75 | if (users.length >= 3) 76 | LeaderboardTopItem( 77 | sNumber: 3, 78 | item: users[2], 79 | ), 80 | ], 81 | ), 82 | ), 83 | LeaderboardTopItem( 84 | sNumber: 1, 85 | first: true, 86 | item: users[0], 87 | ), 88 | ], 89 | ), 90 | const SizedBox(height: 24.0), 91 | const Divider(), 92 | Column( 93 | children: List.generate(leftUsers.length, (index) { 94 | return Column( 95 | children: [ 96 | Padding( 97 | padding: const EdgeInsets.symmetric(vertical: 12.0), 98 | child: LeaderboardItem( 99 | sNumber: index + 4, 100 | item: leftUsers[index], 101 | ), 102 | ), 103 | const Divider(), 104 | ], 105 | ); 106 | }), 107 | ), 108 | ], 109 | ), 110 | ), 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/pages/rewards_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_steps_tracker/di/injection_container.dart'; 4 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/reward_model.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/rewards/rewards_cubit.dart'; 6 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/rewards/rewards_state.dart'; 7 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/widgets/rewards_item.dart'; 8 | import 'package:flutter_steps_tracker/generated/l10n.dart'; 9 | import 'package:flutter_steps_tracker/utilities/constants/assets.dart'; 10 | 11 | class RewardsPage extends StatelessWidget { 12 | const RewardsPage({Key? key}) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return BlocProvider( 17 | create: (_) { 18 | final cubit = getIt(); 19 | cubit.getRewards(); 20 | return cubit; 21 | }, 22 | child: Builder(builder: (context) { 23 | return SingleChildScrollView( 24 | child: Padding( 25 | padding: const EdgeInsets.symmetric( 26 | horizontal: 24.0, 27 | vertical: 8.0, 28 | ), 29 | child: Column( 30 | children: [ 31 | Image.asset( 32 | AppAssets.rewardsBanner, 33 | fit: BoxFit.cover, 34 | height: 200, 35 | ), 36 | const SizedBox(height: 16.0), 37 | Text( 38 | S.of(context).availableRewards, 39 | style: Theme.of(context).textTheme.headline5!.copyWith( 40 | color: Theme.of(context).primaryColor, 41 | ), 42 | ), 43 | const SizedBox(height: 12.0), 44 | BlocBuilder( 45 | bloc: BlocProvider.of(context), 46 | builder: (_, state) { 47 | return state.maybeWhen( 48 | loaded: (rewards) => 49 | _buildList(context, rewards: rewards), 50 | loading: () => _buildList(context, isLoading: true), 51 | orElse: () => _buildList(context), 52 | ); 53 | }, 54 | ), 55 | ], 56 | ), 57 | ), 58 | ); 59 | }), 60 | ); 61 | } 62 | 63 | Widget _buildList(BuildContext context, 64 | {bool isLoading = false, List? rewards}) { 65 | if (isLoading) { 66 | return const Center( 67 | child: CircularProgressIndicator(), 68 | ); 69 | } 70 | debugPrint('Rewards from the fun: ${rewards?.length}'); 71 | return rewards != null && rewards.isNotEmpty 72 | ? Column( 73 | children: rewards 74 | .map( 75 | (e) => Padding( 76 | padding: const EdgeInsets.symmetric(vertical: 4.0), 77 | child: RewardsItem(reward: e), 78 | ), 79 | ) 80 | .toList(), 81 | ) 82 | : Center( 83 | child: Text( 84 | S.current.emptyState, 85 | style: Theme.of(context).textTheme.subtitle1!.copyWith( 86 | color: Theme.of(context).primaryColorDark, 87 | ), 88 | ), 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/widgets/ad_area.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_steps_tracker/generated/l10n.dart'; 4 | import 'package:flutter_steps_tracker/utilities/constants/assets.dart'; 5 | 6 | class AdArea extends StatelessWidget { 7 | const AdArea({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | final size = MediaQuery.of(context).size; 12 | return SizedBox( 13 | width: double.infinity, 14 | height: size.height * 0.12, 15 | child: DecoratedBox( 16 | decoration: BoxDecoration( 17 | color: Theme.of(context).primaryColor, 18 | borderRadius: BorderRadius.circular(24.0), 19 | ), 20 | child: Padding( 21 | padding: const EdgeInsets.all(16.0), 22 | child: Row( 23 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 24 | children: [ 25 | Expanded( 26 | child: Column( 27 | mainAxisAlignment: MainAxisAlignment.center, 28 | children: [ 29 | Text( 30 | S.of(context).moreComingSoon, 31 | style: Theme.of(context).textTheme.headline6!.copyWith( 32 | color: Theme.of(context).scaffoldBackgroundColor, 33 | ), 34 | ), 35 | ], 36 | ), 37 | ), 38 | const SizedBox(width: 8.0), 39 | CachedNetworkImage( 40 | imageUrl: AppAssets.strongArm, 41 | fit: BoxFit.cover, 42 | height: 80, 43 | ), 44 | ], 45 | ), 46 | ), 47 | ), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/widgets/app_bar_area.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_steps_tracker/generated/l10n.dart'; 5 | import 'package:flutter_steps_tracker/utilities/constants/assets.dart'; 6 | import 'package:flutter_steps_tracker/utilities/constants/key_constants.dart'; 7 | import 'package:flutter_steps_tracker/utilities/locale/cubit/utility_cubit.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | class AppBarArea extends StatelessWidget { 11 | const AppBarArea({Key? key}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final utilityCubit = Provider.of(context); 16 | return SafeArea( 17 | child: Padding( 18 | padding: const EdgeInsets.symmetric( 19 | horizontal: 32.0, 20 | vertical: 8.0, 21 | ), 22 | child: Row( 23 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 24 | children: [ 25 | CachedNetworkImage( 26 | imageUrl: AppAssets.logo, 27 | fit: BoxFit.cover, 28 | color: Theme.of(context).primaryColor, 29 | ), 30 | Text( 31 | S.of(context).pedometer, 32 | style: Theme.of(context).textTheme.headline6!.copyWith( 33 | color: Theme.of(context).primaryColor, 34 | ), 35 | ), 36 | Row( 37 | children: [ 38 | InkWell( 39 | onTap: () async => 40 | await Provider.of(context, listen: false) 41 | .switchTheme(), 42 | child: Icon( 43 | utilityCubit.isDark 44 | ? Icons.sunny 45 | : CupertinoIcons.moon_fill, 46 | size: 25, 47 | color: Theme.of(context).primaryColor, 48 | ), 49 | ), 50 | const SizedBox(width: 6.0), 51 | InkWell( 52 | onTap: () async => await utilityCubit.changeLocale( 53 | utilityCubit.locale == KeyConstants.arabicLocale 54 | ? KeyConstants.englishLocale 55 | : KeyConstants.arabicLocale, 56 | ), 57 | child: Text( 58 | utilityCubit.locale == KeyConstants.arabicLocale 59 | ? 'EN' 60 | : 'AR', 61 | style: Theme.of(context) 62 | .textTheme 63 | .headline6! 64 | .copyWith(color: Theme.of(context).primaryColor), 65 | ), 66 | ), 67 | ], 68 | ), 69 | ], 70 | ), 71 | ), 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/widgets/exchanges_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/exchange_history_model.dart'; 4 | import 'package:flutter_steps_tracker/utilities/constants/assets.dart'; 5 | import 'package:flutter_steps_tracker/utilities/constants/enums.dart'; 6 | import 'package:intl/intl.dart'; 7 | 8 | class ExchangesItem extends StatelessWidget { 9 | final ExchangeHistoryModel exchangeHistoryItem; 10 | 11 | const ExchangesItem({ 12 | Key? key, 13 | required this.exchangeHistoryItem, 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final date = 19 | DateFormat.yMMMMd().format(DateTime.parse(exchangeHistoryItem.date)); 20 | return ListTile( 21 | title: Padding( 22 | padding: const EdgeInsets.only(bottom: 6.0), 23 | child: Text( 24 | exchangeHistoryItem.title == ExchangeHistoryTitle.exchange.title 25 | ? exchangeHistoryItem.title 26 | : '${exchangeHistoryItem.points} points ${exchangeHistoryItem.title}', 27 | style: Theme.of(context).textTheme.subtitle1!.copyWith( 28 | fontWeight: FontWeight.w600, 29 | color: Theme.of(context).listTileTheme.textColor, 30 | ), 31 | ), 32 | ), 33 | subtitle: Text( 34 | date, 35 | style: Theme.of(context).textTheme.subtitle2!.copyWith( 36 | color: Colors.grey, 37 | ), 38 | ), 39 | leading: CachedNetworkImage( 40 | imageUrl: 41 | exchangeHistoryItem.title == ExchangeHistoryTitle.exchange.title 42 | ? AppAssets.exchangesIcon 43 | : AppAssets.rewardsIcon, 44 | height: 50, 45 | width: 50, 46 | ), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/widgets/get_radial_gauge.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_steps_tracker/di/injection_container.dart'; 4 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/home/home_cubit.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/home/home_state.dart'; 6 | import 'package:flutter_steps_tracker/generated/l10n.dart'; 7 | import 'package:syncfusion_flutter_gauges/gauges.dart'; 8 | 9 | class GetRadialGauge extends StatefulWidget { 10 | const GetRadialGauge({Key? key}) : super(key: key); 11 | 12 | @override 13 | State createState() => _GetRadialGaugeState(); 14 | } 15 | 16 | class _GetRadialGaugeState extends State { 17 | late HomeCubit _homeCubit; 18 | 19 | @override 20 | void initState() { 21 | super.initState(); 22 | _homeCubit = getIt(); 23 | _homeCubit.initPlatformState(); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return BlocConsumer( 29 | bloc: _homeCubit, 30 | listener: (context, state) { 31 | state.maybeWhen( 32 | feedbackGain: (steps) { 33 | final snackBar = SnackBar( 34 | content: Text(S.of(context).gainMorePoints), 35 | ); 36 | ScaffoldMessenger.of(context).showSnackBar(snackBar); 37 | }, 38 | orElse: () {}, 39 | ); 40 | }, 41 | builder: (context, state) { 42 | return state.maybeWhen( 43 | loaded: (steps) => _buildRadialGauge( 44 | context, 45 | steps: steps, 46 | ), 47 | feedbackGain: (steps) => _buildRadialGauge( 48 | context, 49 | steps: steps, 50 | ), 51 | orElse: () => _buildRadialGauge(context), 52 | ); 53 | }, 54 | ); 55 | } 56 | 57 | Widget _buildRadialGauge( 58 | BuildContext context, { 59 | String steps = '0', 60 | }) { 61 | return SfRadialGauge( 62 | enableLoadingAnimation: true, 63 | title: GaugeTitle( 64 | text: S.of(context).totalStepsToday, 65 | textStyle: Theme.of(context).textTheme.headline5!.copyWith( 66 | color: Theme.of(context).primaryColor, 67 | ), 68 | ), 69 | axes: [ 70 | RadialAxis( 71 | axisLineStyle: AxisLineStyle( 72 | color: Theme.of(context).primaryColorDark, 73 | dashArray: const [8, 2], 74 | ), 75 | axisLabelStyle: GaugeTextStyle( 76 | color: Theme.of(context).primaryColorDark, 77 | ), 78 | annotations: [ 79 | GaugeAnnotation( 80 | widget: Text.rich( 81 | TextSpan(children: [ 82 | TextSpan( 83 | text: '$steps\n', 84 | style: Theme.of(context).textTheme.headline2!.copyWith( 85 | color: Theme.of(context).primaryColor, 86 | ), 87 | ), 88 | TextSpan( 89 | text: '${S.of(context).stepGoal}\n', 90 | style: Theme.of(context).textTheme.subtitle1!.copyWith( 91 | color: Theme.of(context).backgroundColor, 92 | fontWeight: FontWeight.w400, 93 | ), 94 | ), 95 | TextSpan( 96 | text: '8000\n', 97 | style: Theme.of(context).textTheme.subtitle1!.copyWith( 98 | color: Theme.of(context).backgroundColor, 99 | fontWeight: FontWeight.w400, 100 | ), 101 | ), 102 | ]), 103 | textAlign: TextAlign.center, 104 | ), 105 | ), 106 | ], 107 | pointers: [ 108 | RangePointer( 109 | // TODO: Dummy equation for now until targeting each one's target 110 | // Our target is 8000 steps daily, do more sports!!!! 111 | value: ((double.tryParse(steps)!) / 8000) * 100, 112 | color: Theme.of(context).primaryColor, 113 | dashArray: const [8, 2], 114 | ), 115 | ], 116 | ), 117 | ], 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/widgets/health_points_and_calories.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/home/home_cubit.dart'; 4 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/home/home_state.dart'; 5 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/widgets/health_points_and_calories_item.dart'; 6 | import 'package:flutter_steps_tracker/generated/l10n.dart'; 7 | 8 | class HealthPointsAndCalories extends StatelessWidget { 9 | const HealthPointsAndCalories({Key? key}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return BlocBuilder( 14 | bloc: BlocProvider.of(context), 15 | builder: (context, state) { 16 | return state.maybeWhen( 17 | stepsAndPointsLoading: () => 18 | _buildHealthRow(context, isLoading: true), 19 | stepsAndPointsLoaded: (steps, healthPoints) => _buildHealthRow( 20 | context, 21 | steps: steps, 22 | healthPoints: healthPoints, 23 | ), 24 | orElse: () => _buildHealthRow(context), 25 | ); 26 | }, 27 | ); 28 | } 29 | 30 | Widget _buildHealthRow( 31 | BuildContext context, { 32 | bool isLoading = false, 33 | int steps = 0, 34 | int healthPoints = 0, 35 | }) { 36 | return Row( 37 | mainAxisAlignment: MainAxisAlignment.center, 38 | children: [ 39 | Expanded( 40 | child: HealthPointsAndCaloriesItem( 41 | mainTitle: S.of(context).healthPoints, 42 | number: isLoading ? '-' : healthPoints.toString(), 43 | iconData: Icons.shopping_bag, 44 | color: Theme.of(context).primaryColor, 45 | unit: '', 46 | ), 47 | ), 48 | const SizedBox(width: 16.0), 49 | Expanded( 50 | child: HealthPointsAndCaloriesItem( 51 | mainTitle: S.of(context).totalSteps, 52 | number: isLoading ? '-' : steps.toString(), 53 | iconData: Icons.bubble_chart_rounded, 54 | color: Theme.of(context).backgroundColor, 55 | ), 56 | ), 57 | ], 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/widgets/health_points_and_calories_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class HealthPointsAndCaloriesItem extends StatelessWidget { 4 | final Color color; 5 | final String mainTitle; 6 | final IconData iconData; 7 | final String number; 8 | final String unit; 9 | 10 | const HealthPointsAndCaloriesItem({ 11 | Key? key, 12 | this.color = Colors.blue, 13 | required this.mainTitle, 14 | required this.iconData, 15 | this.number = '-', 16 | this.unit = '', 17 | }) : super(key: key); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return DecoratedBox( 22 | decoration: BoxDecoration( 23 | color: color, 24 | borderRadius: BorderRadius.circular(16.0), 25 | ), 26 | child: Padding( 27 | padding: const EdgeInsets.all(18.0), 28 | child: Column( 29 | crossAxisAlignment: CrossAxisAlignment.center, 30 | children: [ 31 | Row( 32 | mainAxisAlignment: MainAxisAlignment.center, 33 | children: [ 34 | Icon( 35 | iconData, 36 | size: 30, 37 | ), 38 | const SizedBox(width: 4.0), 39 | Text.rich( 40 | TextSpan( 41 | children: [ 42 | TextSpan( 43 | text: number.toString(), 44 | style: Theme.of(context).textTheme.headline5!.copyWith( 45 | color: Theme.of(context).scaffoldBackgroundColor, 46 | ), 47 | ), 48 | if (unit.isNotEmpty) 49 | TextSpan( 50 | text: ' $unit', 51 | style: Theme.of(context) 52 | .textTheme 53 | .subtitle1! 54 | .copyWith( 55 | color: 56 | Theme.of(context).scaffoldBackgroundColor, 57 | ), 58 | ), 59 | ], 60 | ), 61 | ) 62 | ], 63 | ), 64 | const SizedBox(height: 8.0), 65 | Text( 66 | mainTitle, 67 | style: Theme.of(context).textTheme.subtitle1!.copyWith( 68 | color: Theme.of(context).scaffoldBackgroundColor, 69 | ), 70 | ), 71 | ], 72 | ), 73 | ), 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/widgets/leaderboard_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_steps_tracker/core/data/models/user_model.dart'; 5 | import 'package:flutter_steps_tracker/utilities/constants/assets.dart'; 6 | 7 | class LeaderboardItem extends StatelessWidget { 8 | final UserModel item; 9 | final int sNumber; 10 | 11 | const LeaderboardItem({ 12 | Key? key, 13 | required this.item, 14 | required this.sNumber, 15 | }) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Row( 20 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 21 | children: [ 22 | Column( 23 | children: [ 24 | Text( 25 | sNumber.toString(), 26 | style: Theme.of(context).textTheme.subtitle1!.copyWith( 27 | color: Theme.of(context).primaryColorDark, 28 | ), 29 | ), 30 | Icon( 31 | CupertinoIcons.arrowtriangle_up_fill, 32 | color: Theme.of(context).primaryColorLight, 33 | size: 20, 34 | ), 35 | ], 36 | ), 37 | const SizedBox(width: 16.0), 38 | Expanded( 39 | child: Row( 40 | children: [ 41 | const CircleAvatar( 42 | radius: 20, 43 | backgroundImage: CachedNetworkImageProvider( 44 | AppAssets.dummyUserImage, 45 | ), 46 | ), 47 | const SizedBox(width: 12.0), 48 | Text( 49 | item.name, 50 | style: Theme.of(context).textTheme.subtitle1!.copyWith( 51 | color: Theme.of(context).primaryColorDark, 52 | fontWeight: FontWeight.w600, 53 | ), 54 | ), 55 | const Spacer(), 56 | Text( 57 | item.totalSteps.toString(), 58 | style: Theme.of(context).textTheme.headline6!.copyWith( 59 | color: Theme.of(context).primaryColorLight, 60 | fontWeight: FontWeight.w600, 61 | ), 62 | ), 63 | ], 64 | ), 65 | ), 66 | ], 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/widgets/leaderboard_top_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_steps_tracker/core/data/models/user_model.dart'; 5 | import 'package:flutter_steps_tracker/utilities/constants/assets.dart'; 6 | 7 | class LeaderboardTopItem extends StatelessWidget { 8 | final UserModel item; 9 | final int sNumber; 10 | final bool first; 11 | 12 | const LeaderboardTopItem({ 13 | Key? key, 14 | required this.item, 15 | required this.sNumber, 16 | this.first = false, 17 | }) : super(key: key); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Column( 22 | children: [ 23 | Text( 24 | sNumber.toString(), 25 | style: Theme.of(context) 26 | .textTheme 27 | .headline6! 28 | .copyWith(color: Theme.of(context).primaryColorDark), 29 | ), 30 | const SizedBox(height: 4.0), 31 | Icon( 32 | CupertinoIcons.arrowtriangle_up_fill, 33 | color: Theme.of(context).primaryColorLight, 34 | ), 35 | const SizedBox(height: 8.0), 36 | CircleAvatar( 37 | radius: first ? 75 : 65, 38 | backgroundColor: Theme.of(context).primaryColorLight, 39 | child: CircleAvatar( 40 | radius: first ? 70 : 60, 41 | backgroundImage: const CachedNetworkImageProvider( 42 | AppAssets.dummyUserImage, 43 | ), 44 | ), 45 | ), 46 | const SizedBox(height: 8.0), 47 | Text( 48 | item.name, 49 | style: Theme.of(context).textTheme.subtitle1!.copyWith( 50 | color: Theme.of(context).primaryColorDark, 51 | fontWeight: FontWeight.w600, 52 | ), 53 | ), 54 | Text( 55 | item.totalSteps.toString(), 56 | style: Theme.of(context).textTheme.headline6!.copyWith( 57 | color: Theme.of(context).primaryColorLight, 58 | fontWeight: FontWeight.w600, 59 | ), 60 | ), 61 | ], 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/features/bottom_navbar/presentation/widgets/rewards_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:flutter_steps_tracker/core/presentation/widgets/show_alert_dialog.dart'; 5 | import 'package:flutter_steps_tracker/di/injection_container.dart'; 6 | import 'package:flutter_steps_tracker/features/bottom_navbar/data/models/reward_model.dart'; 7 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/rewards/rewards_cubit.dart'; 8 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/manager/rewards/rewards_state.dart'; 9 | import 'package:flutter_steps_tracker/generated/l10n.dart'; 10 | 11 | class RewardsItem extends StatefulWidget { 12 | final RewardModel reward; 13 | 14 | const RewardsItem({ 15 | Key? key, 16 | required this.reward, 17 | }) : super(key: key); 18 | 19 | @override 20 | State createState() => _RewardsItemState(); 21 | } 22 | 23 | class _RewardsItemState extends State { 24 | late RewardsCubit _cubit; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | _cubit = getIt(); 30 | _cubit.getUserPoints(); 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return Wrap( 36 | children: [ 37 | Card( 38 | shape: RoundedRectangleBorder( 39 | borderRadius: BorderRadius.circular(16.0), 40 | ), 41 | child: Padding( 42 | padding: const EdgeInsets.all(16.0), 43 | child: Row( 44 | children: [ 45 | CachedNetworkImage( 46 | imageUrl: widget.reward.imageUrl, 47 | fit: BoxFit.cover, 48 | height: 80, 49 | ), 50 | const SizedBox(width: 12.0), 51 | Expanded( 52 | child: Column( 53 | crossAxisAlignment: CrossAxisAlignment.start, 54 | mainAxisAlignment: MainAxisAlignment.center, 55 | children: [ 56 | Text( 57 | widget.reward.name, 58 | style: Theme.of(context).textTheme.headline6!.copyWith( 59 | fontWeight: FontWeight.bold, 60 | ), 61 | ), 62 | const SizedBox(height: 4.0), 63 | Text.rich( 64 | TextSpan( 65 | children: [ 66 | TextSpan( 67 | text: widget.reward.description, 68 | style: Theme.of(context).textTheme.subtitle1, 69 | ), 70 | TextSpan( 71 | text: ' ${widget.reward.points} Points!', 72 | style: Theme.of(context) 73 | .textTheme 74 | .subtitle1! 75 | .copyWith( 76 | fontWeight: FontWeight.bold, 77 | ), 78 | ), 79 | ], 80 | ), 81 | ), 82 | const SizedBox(height: 6.0), 83 | BlocBuilder( 84 | bloc: _cubit, 85 | buildWhen: (prev, current) => 86 | current is UserDataLoading || 87 | current is UserDataLoaded, 88 | builder: (context, state) { 89 | return state.maybeWhen( 90 | userDataLoading: () => _buildEarnButton( 91 | context, 92 | isLoading: true, 93 | ), 94 | userDataLoaded: (points) => _buildEarnButton( 95 | context, 96 | points: points, 97 | ), 98 | orElse: () => _buildEarnButton(context), 99 | ); 100 | }, 101 | ), 102 | ], 103 | ), 104 | ), 105 | ], 106 | ), 107 | ), 108 | ), 109 | ], 110 | ); 111 | } 112 | 113 | // TODO: Refactor this to make it a separate widget for all the buttons 114 | Widget _buildEarnButton( 115 | BuildContext context, { 116 | bool isLoading = false, 117 | int points = 0, 118 | }) { 119 | return SizedBox( 120 | width: double.infinity, 121 | child: ElevatedButton( 122 | onPressed: !isLoading 123 | ? () { 124 | if (widget.reward.points <= points) { 125 | showAlertDialog( 126 | context, 127 | title: S.current.qrCode, 128 | contentWidget: Padding( 129 | padding: const EdgeInsets.all(8.0), 130 | child: Column( 131 | children: [ 132 | CachedNetworkImage( 133 | imageUrl: widget.reward.qrCode, 134 | fit: BoxFit.cover, 135 | ), 136 | const SizedBox(height: 16.0), 137 | Text( 138 | S.current.scanQrCode, 139 | style: Theme.of(context).textTheme.subtitle1, 140 | ), 141 | ], 142 | ), 143 | ), 144 | defaultActionText: S.current.dummyDone, 145 | cubit: _cubit, 146 | defaultAction: () async { 147 | await _cubit.earnAReward(widget.reward); 148 | }, 149 | ); 150 | } else { 151 | showAlertDialog( 152 | context, 153 | title: S.current.notice, 154 | content: S.current.pointsLessThanItem, 155 | defaultActionText: S.current.done, 156 | ); 157 | } 158 | } 159 | : null, 160 | style: ElevatedButton.styleFrom( 161 | primary: Theme.of(context).scaffoldBackgroundColor, 162 | shape: RoundedRectangleBorder( 163 | borderRadius: BorderRadius.circular(8.0), 164 | side: BorderSide( 165 | color: Theme.of(context).primaryColor, 166 | ), 167 | ), 168 | ), 169 | child: !isLoading 170 | ? Text( 171 | S.current.earn, 172 | style: Theme.of(context).textTheme.subtitle1!.copyWith( 173 | color: Theme.of(context).primaryColor, 174 | ), 175 | ) 176 | : const CircularProgressIndicator(), 177 | ), 178 | ); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /lib/features/intro/data/data_sources/auth_local_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_steps_tracker/core/data/models/user_model.dart'; 4 | import 'package:flutter_steps_tracker/utilities/constants/key_constants.dart'; 5 | import 'package:injectable/injectable.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | 8 | abstract class AuthLocalDataSource { 9 | Future persistAuth(UserModel user); 10 | 11 | Future currentUser(); 12 | } 13 | 14 | @Singleton(as: AuthLocalDataSource) 15 | class AuthLocalDataSourceImpl extends AuthLocalDataSource { 16 | final SharedPreferences _sharedPreferences; 17 | 18 | AuthLocalDataSourceImpl(this._sharedPreferences); 19 | 20 | @override 21 | Future currentUser() async { 22 | if (_sharedPreferences.containsKey(KeyConstants.currentUser)) { 23 | final currentUserAsJSON = 24 | json.decode(_sharedPreferences.getString(KeyConstants.currentUser)!); 25 | final user = UserModel.fromMap( 26 | currentUserAsJSON, 27 | currentUserAsJSON['uid'] ?? '', 28 | ); 29 | return user; 30 | } 31 | return null; 32 | } 33 | 34 | @override 35 | Future persistAuth(UserModel user) async { 36 | final encodedJson = json.encode(user.toMap()); 37 | _sharedPreferences.setString( 38 | KeyConstants.currentUser, 39 | encodedJson.toString(), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/features/intro/data/data_sources/auth_remote_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:flutter_steps_tracker/core/data/data_sources/data_sources_body.dart'; 3 | import 'package:flutter_steps_tracker/features/intro/data/services/auth_services.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | 6 | abstract class AuthRemoteDataSource { 7 | Future signInAnonymously(); 8 | } 9 | 10 | @Singleton(as: AuthRemoteDataSource) 11 | class AuthRemoteDataSourceImpl extends AuthRemoteDataSource { 12 | final AuthBase authBase; 13 | 14 | AuthRemoteDataSourceImpl({required this.authBase}); 15 | 16 | @override 17 | Future signInAnonymously() async => returnOrThrow( 18 | () => authBase.signInAnonymously(), 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /lib/features/intro/data/mappers/user_entity_to_model_mapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_steps_tracker/core/data/data_sources/database.dart'; 2 | import 'package:flutter_steps_tracker/core/data/models/user_model.dart'; 3 | import 'package:flutter_steps_tracker/features/intro/domain/entities/user_entity.dart'; 4 | 5 | extension UserEntityToModelMapper on UserEntity { 6 | UserModel toModel() { 7 | return UserModel( 8 | uid: uid ?? documentIdFromLocalGenerator(), 9 | name: name, 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/features/intro/data/repositories/auth_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_steps_tracker/core/data/data_sources/database.dart'; 3 | import 'package:flutter_steps_tracker/core/data/error/exceptions/application_exception.dart'; 4 | import 'package:flutter_steps_tracker/core/data/error/failures/application_failure.dart'; 5 | import 'package:flutter_steps_tracker/features/intro/data/data_sources/auth_local_data_source.dart'; 6 | import 'package:flutter_steps_tracker/features/intro/data/data_sources/auth_remote_data_source.dart'; 7 | import 'package:flutter_steps_tracker/features/intro/data/mappers/user_entity_to_model_mapper.dart'; 8 | import 'package:flutter_steps_tracker/features/intro/domain/entities/user_entity.dart'; 9 | import 'package:flutter_steps_tracker/features/intro/domain/repositories/auth_repository.dart'; 10 | import 'package:flutter_steps_tracker/utilities/constants/enums.dart'; 11 | import 'package:injectable/injectable.dart'; 12 | 13 | @Singleton(as: AuthRepository) 14 | class AuthRepositoryImpl extends AuthRepository { 15 | final AuthRemoteDataSource _authRemoteDataSource; 16 | final AuthLocalDataSource _authLocalDataSource; 17 | final Database _database; 18 | 19 | AuthRepositoryImpl( 20 | this._authRemoteDataSource, 21 | this._authLocalDataSource, 22 | this._database, 23 | ); 24 | 25 | @override 26 | Future> signInAnonymously(String name) async { 27 | try { 28 | final user = await _authRemoteDataSource.signInAnonymously(); 29 | final newUser = UserEntity( 30 | name: name, 31 | uid: user?.uid, 32 | ); 33 | await _authLocalDataSource.persistAuth(newUser.toModel()); 34 | await _database.setUserData(newUser.toModel()); 35 | return const Right(true); 36 | } on ApplicationException catch (e) { 37 | return Left( 38 | firebaseExceptionsDecoder(e), 39 | ); 40 | } 41 | } 42 | 43 | @override 44 | Future hasAnAccount() async { 45 | final currentUser = await _authLocalDataSource.currentUser(); 46 | if (currentUser != null) { 47 | return AuthStatus.authenticated; 48 | } 49 | return AuthStatus.unAuthenticated; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/features/intro/data/services/auth_services.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | abstract class AuthBase { 5 | Future signInAnonymously(); 6 | } 7 | 8 | @Singleton(as: AuthBase) 9 | class Auth implements AuthBase { 10 | final _firebaseAuth = FirebaseAuth.instance; 11 | 12 | @override 13 | Future signInAnonymously() async { 14 | if (_firebaseAuth.currentUser != null) { 15 | await _firebaseAuth.currentUser!.delete(); 16 | } 17 | final userCredential = await _firebaseAuth.signInAnonymously(); 18 | return userCredential.user; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/features/intro/domain/entities/user_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class UserEntity extends Equatable { 4 | final String? uid; 5 | final String name; 6 | 7 | const UserEntity({ 8 | this.uid, 9 | required this.name, 10 | }); 11 | 12 | @override 13 | List get props => [uid, name]; 14 | } 15 | -------------------------------------------------------------------------------- /lib/features/intro/domain/repositories/auth_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_steps_tracker/core/data/error/failures/application_failure.dart'; 3 | import 'package:flutter_steps_tracker/utilities/constants/enums.dart'; 4 | 5 | abstract class AuthRepository { 6 | Future> signInAnonymously(String name); 7 | Future hasAnAccount(); 8 | } 9 | -------------------------------------------------------------------------------- /lib/features/intro/domain/use_cases/auth_status_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_steps_tracker/core/domain/use_cases/use_case.dart'; 2 | import 'package:flutter_steps_tracker/features/intro/domain/repositories/auth_repository.dart'; 3 | import 'package:flutter_steps_tracker/utilities/constants/enums.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | 6 | @injectable 7 | class AuthStatusUseCase extends UseCase, NoParams> { 8 | final AuthRepository _authRepository; 9 | 10 | AuthStatusUseCase(this._authRepository); 11 | 12 | @override 13 | Future call(NoParams params) async => 14 | await _authRepository.hasAnAccount(); 15 | } 16 | -------------------------------------------------------------------------------- /lib/features/intro/domain/use_cases/sign_in_anonymously_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_steps_tracker/core/data/error/failures/application_failure.dart'; 3 | import 'package:flutter_steps_tracker/core/domain/use_cases/use_case.dart'; 4 | import 'package:flutter_steps_tracker/features/intro/domain/repositories/auth_repository.dart'; 5 | import 'package:injectable/injectable.dart'; 6 | 7 | @injectable 8 | class SignInAnonymouslyUseCase 9 | extends UseCase>, String> { 10 | final AuthRepository authRepository; 11 | 12 | SignInAnonymouslyUseCase({required this.authRepository}); 13 | 14 | @override 15 | Future> call(String params) async => 16 | await authRepository.signInAnonymously( 17 | params, 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /lib/features/intro/presentation/manager/auth_actions/auth_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:flutter_steps_tracker/core/data/error/failures/firebase_auth_failure.dart'; 3 | import 'package:flutter_steps_tracker/features/intro/domain/use_cases/sign_in_anonymously_use_case.dart'; 4 | import 'package:flutter_steps_tracker/features/intro/presentation/manager/auth_actions/auth_state.dart'; 5 | import 'package:flutter_steps_tracker/generated/l10n.dart'; 6 | import 'package:injectable/injectable.dart'; 7 | 8 | @singleton 9 | class AuthCubit extends Cubit { 10 | final SignInAnonymouslyUseCase _signInAnonymouslyUseCase; 11 | 12 | AuthCubit( 13 | this._signInAnonymouslyUseCase, 14 | ) : super(const AuthState.initial()); 15 | 16 | Future signInAnonymously({required String name}) async { 17 | emit(const AuthState.loading()); 18 | final result = await _signInAnonymouslyUseCase(name); 19 | emit( 20 | result.fold( 21 | (failure) { 22 | if (failure is FirebaseAuthFailure) { 23 | failure.maybeWhen( 24 | orElse: () => 25 | AuthState.error(message: S.current.somethingWentWrong), 26 | operationNotAllowed: (message) => 27 | AuthState.error(message: message)); 28 | } 29 | return AuthState.error(message: S.current.somethingWentWrong); 30 | }, 31 | (_) => const AuthState.loggedIn(), 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/features/intro/presentation/manager/auth_actions/auth_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'auth_state.freezed.dart'; 4 | 5 | @freezed 6 | abstract class AuthState with _$AuthState { 7 | const factory AuthState.initial() = Initial; 8 | 9 | const factory AuthState.loading() = Loading; 10 | 11 | const factory AuthState.loggedIn() = LoggedIn; 12 | 13 | const factory AuthState.error({required String message}) = Error; 14 | } 15 | -------------------------------------------------------------------------------- /lib/features/intro/presentation/manager/auth_status/auth_status_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:flutter_steps_tracker/core/domain/use_cases/use_case.dart'; 3 | import 'package:flutter_steps_tracker/features/intro/domain/use_cases/auth_status_use_case.dart'; 4 | import 'package:flutter_steps_tracker/features/intro/presentation/manager/auth_status/auth_status_state.dart'; 5 | import 'package:flutter_steps_tracker/utilities/constants/enums.dart'; 6 | import 'package:injectable/injectable.dart'; 7 | 8 | @singleton 9 | class AuthStatusCubit extends Cubit { 10 | final AuthStatusUseCase _authStatusUseCase; 11 | 12 | AuthStatusCubit( 13 | this._authStatusUseCase, 14 | ) : super( 15 | const AuthStatusState.unAuthenticated(), 16 | ); 17 | 18 | Future checkAuthStatus() async { 19 | final status = await _authStatusUseCase(NoParams()); 20 | switch (status) { 21 | case AuthStatus.authenticated: 22 | emit(const AuthStatusState.authenticated()); 23 | break; 24 | case AuthStatus.unAuthenticated: 25 | emit(const AuthStatusState.unAuthenticated()); 26 | break; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/features/intro/presentation/manager/auth_status/auth_status_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'auth_status_state.freezed.dart'; 4 | 5 | @freezed 6 | abstract class AuthStatusState with _$AuthStatusState { 7 | const factory AuthStatusState.authenticated() = Authenticated; 8 | 9 | const factory AuthStatusState.unAuthenticated() = UnAuthenticated; 10 | } 11 | -------------------------------------------------------------------------------- /lib/features/intro/presentation/pages/intro_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:flutter_steps_tracker/di/injection_container.dart'; 5 | import 'package:flutter_steps_tracker/features/intro/presentation/manager/auth_actions/auth_cubit.dart'; 6 | import 'package:flutter_steps_tracker/features/intro/presentation/manager/auth_actions/auth_state.dart'; 7 | import 'package:flutter_steps_tracker/features/intro/presentation/manager/auth_status/auth_status_cubit.dart'; 8 | import 'package:flutter_steps_tracker/generated/l10n.dart'; 9 | import 'package:flutter_steps_tracker/utilities/constants/assets.dart'; 10 | 11 | class IntroPage extends StatefulWidget { 12 | const IntroPage({Key? key}) : super(key: key); 13 | 14 | @override 15 | State createState() => _IntroPageState(); 16 | } 17 | 18 | class _IntroPageState extends State { 19 | final _formKey = GlobalKey(); 20 | final _nameController = TextEditingController(); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return BlocProvider( 25 | create: (context) => getIt(), 26 | child: Builder( 27 | builder: (context) { 28 | return Scaffold( 29 | resizeToAvoidBottomInset: true, 30 | // appBar: AppBar(), 31 | body: Stack( 32 | children: [ 33 | Image.asset( 34 | AppAssets.manInBackgroundIntro, 35 | fit: BoxFit.cover, 36 | height: double.infinity, 37 | width: double.infinity, 38 | ), 39 | SizedBox( 40 | height: double.infinity, 41 | width: double.infinity, 42 | child: Opacity( 43 | opacity: 0.8, 44 | child: DecoratedBox( 45 | decoration: BoxDecoration( 46 | color: Theme.of(context).primaryColor, 47 | ), 48 | ), 49 | ), 50 | ), 51 | SafeArea( 52 | child: Padding( 53 | padding: const EdgeInsets.symmetric( 54 | horizontal: 24.0, 55 | vertical: 48.0, 56 | ), 57 | child: Center( 58 | child: BlocConsumer( 59 | listener: (context, state) { 60 | state.maybeWhen( 61 | loggedIn: () { 62 | final cubit = 63 | BlocProvider.of(context); 64 | cubit.checkAuthStatus(); 65 | // Navigator.of(context).pop(); 66 | }, 67 | error: (message) { 68 | // showCustomAlertDialog( 69 | // context, 70 | // message, 71 | // isErrorDialog: true, 72 | // errorContext: S.of(context).login, 73 | // ); 74 | }, 75 | orElse: () {}); 76 | }, 77 | builder: (context, state) { 78 | return state.maybeWhen( 79 | loading: () => _buildColumn(true, context), 80 | orElse: () => _buildColumn(false, context), 81 | ); 82 | }, 83 | ), 84 | ), 85 | ), 86 | ), 87 | ], 88 | ), 89 | ); 90 | }, 91 | ), 92 | ); 93 | } 94 | 95 | Widget _buildColumn(bool isLoading, BuildContext context) { 96 | return Form( 97 | key: _formKey, 98 | child: Column( 99 | children: [ 100 | const Spacer( 101 | flex: 1, 102 | ), 103 | CachedNetworkImage( 104 | imageUrl: AppAssets.logo, 105 | fit: BoxFit.cover, 106 | color: Theme.of(context).scaffoldBackgroundColor, 107 | height: 180, 108 | ), 109 | Text( 110 | S.of(context).allInOneTrack, 111 | style: Theme.of(context).textTheme.subtitle1!.copyWith( 112 | color: Theme.of(context).scaffoldBackgroundColor, 113 | ), 114 | ), 115 | const Spacer( 116 | flex: 2, 117 | ), 118 | TextFormField( 119 | controller: _nameController, 120 | validator: (val) => 121 | val!.isEmpty ? S.of(context).enterYourName : null, 122 | autocorrect: false, 123 | decoration: InputDecoration( 124 | hintText: S.of(context).enterYourName, 125 | ), 126 | ), 127 | const SizedBox(height: 16.0), 128 | InkWell( 129 | onTap: () async { 130 | if (_formKey.currentState!.validate()) { 131 | await BlocProvider.of(context).signInAnonymously( 132 | name: _nameController.text, 133 | ); 134 | } 135 | }, 136 | child: Container( 137 | height: 50, 138 | width: double.infinity, 139 | decoration: BoxDecoration( 140 | border: Border.all( 141 | color: Theme.of(context).scaffoldBackgroundColor, 142 | ), 143 | borderRadius: BorderRadius.circular(24.0), 144 | ), 145 | child: Center( 146 | child: !isLoading 147 | ? Text( 148 | S.of(context).startUsingSteps, 149 | style: Theme.of(context).textTheme.subtitle1!.copyWith( 150 | color: Theme.of(context).scaffoldBackgroundColor, 151 | fontWeight: FontWeight.bold, 152 | ), 153 | ) 154 | : const CircularProgressIndicator(), 155 | ), 156 | ), 157 | ), 158 | ], 159 | ), 160 | ); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /lib/generated/intl/messages_all.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | // Ignore issues from commonly used lints in this file. 6 | // ignore_for_file:implementation_imports, file_names, unnecessary_new 7 | // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering 8 | // ignore_for_file:argument_type_not_assignable, invalid_assignment 9 | // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases 10 | // ignore_for_file:comment_references 11 | 12 | import 'dart:async'; 13 | 14 | import 'package:intl/intl.dart'; 15 | import 'package:intl/message_lookup_by_library.dart'; 16 | import 'package:intl/src/intl_helpers.dart'; 17 | 18 | import 'messages_ar.dart' as messages_ar; 19 | import 'messages_en.dart' as messages_en; 20 | 21 | typedef Future LibraryLoader(); 22 | Map _deferredLibraries = { 23 | 'ar': () => new Future.value(null), 24 | 'en': () => new Future.value(null), 25 | }; 26 | 27 | MessageLookupByLibrary? _findExact(String localeName) { 28 | switch (localeName) { 29 | case 'ar': 30 | return messages_ar.messages; 31 | case 'en': 32 | return messages_en.messages; 33 | default: 34 | return null; 35 | } 36 | } 37 | 38 | /// User programs should call this before using [localeName] for messages. 39 | Future initializeMessages(String localeName) async { 40 | var availableLocale = Intl.verifiedLocale( 41 | localeName, (locale) => _deferredLibraries[locale] != null, 42 | onFailure: (_) => null); 43 | if (availableLocale == null) { 44 | return new Future.value(false); 45 | } 46 | var lib = _deferredLibraries[availableLocale]; 47 | await (lib == null ? new Future.value(false) : lib()); 48 | initializeInternalMessageLookup(() => new CompositeMessageLookup()); 49 | messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); 50 | return new Future.value(true); 51 | } 52 | 53 | bool _messagesExistFor(String locale) { 54 | try { 55 | return _findExact(locale) != null; 56 | } catch (e) { 57 | return false; 58 | } 59 | } 60 | 61 | MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { 62 | var actualLocale = 63 | Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); 64 | if (actualLocale == null) return null; 65 | return _findExact(actualLocale); 66 | } 67 | -------------------------------------------------------------------------------- /lib/generated/intl/messages_ar.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that provides messages for a ar locale. All the 3 | // messages from the main program should be duplicated here with the same 4 | // function name. 5 | 6 | // Ignore issues from commonly used lints in this file. 7 | // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new 8 | // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering 9 | // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases 10 | // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes 11 | // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes 12 | 13 | import 'package:intl/intl.dart'; 14 | import 'package:intl/message_lookup_by_library.dart'; 15 | 16 | final messages = new MessageLookup(); 17 | 18 | typedef String MessageIfAbsent(String messageStr, List args); 19 | 20 | class MessageLookup extends MessageLookupByLibrary { 21 | String get localeName => 'ar'; 22 | 23 | final messages = _notInlinedMessages(_notInlinedMessages); 24 | static Map _notInlinedMessages(_) => { 25 | "allInOneTrack": 26 | MessageLookupByLibrary.simpleMessage("كل ما تحتاجه لتتبع نشاطك!"), 27 | "availableRewards": 28 | MessageLookupByLibrary.simpleMessage("الجوائز المتاحة"), 29 | "done": MessageLookupByLibrary.simpleMessage("إنهاء"), 30 | "dummyDone": MessageLookupByLibrary.simpleMessage("شراء وهمي"), 31 | "earn": MessageLookupByLibrary.simpleMessage("احصل عليه"), 32 | "emptyState": 33 | MessageLookupByLibrary.simpleMessage("لا يوجد بيانات متاحة!"), 34 | "enterYourName": 35 | MessageLookupByLibrary.simpleMessage("يرجى كتابة اسمك"), 36 | "exchanges": MessageLookupByLibrary.simpleMessage("تحويلات"), 37 | "gainMorePoints": MessageLookupByLibrary.simpleMessage( 38 | "تهانينا! لقد حصلت على المزيد من النقاط"), 39 | "healthPoints": MessageLookupByLibrary.simpleMessage("نقاط الصحة"), 40 | "home": MessageLookupByLibrary.simpleMessage("الرئيسية"), 41 | "leaderboard": MessageLookupByLibrary.simpleMessage("ترتيب"), 42 | "moreComingSoon": 43 | MessageLookupByLibrary.simpleMessage("انتظروا المزيد قريبًا!"), 44 | "notice": MessageLookupByLibrary.simpleMessage("تنويه"), 45 | "pedometer": MessageLookupByLibrary.simpleMessage("متتبع الخطوات"), 46 | "points": MessageLookupByLibrary.simpleMessage("نقاط"), 47 | "pointsLessThanItem": MessageLookupByLibrary.simpleMessage( 48 | "نقاطك الحالية لا تكفي لشراء هذا المنتج، قم بالمشي أكثر وجرب مرة أخرى!"), 49 | "qrCode": MessageLookupByLibrary.simpleMessage("رمز الاستجابة السريعة"), 50 | "rewards": MessageLookupByLibrary.simpleMessage("جوائز"), 51 | "scanQrCode": MessageLookupByLibrary.simpleMessage( 52 | "امسح رمز الاستجابة السريعة وسيتم سحب النقاط"), 53 | "somethingWentWrong": 54 | MessageLookupByLibrary.simpleMessage("ثمة خطأ ما!"), 55 | "startUsingSteps": 56 | MessageLookupByLibrary.simpleMessage("بدء الاستخدام"), 57 | "stepGoal": MessageLookupByLibrary.simpleMessage("هدف اليوم"), 58 | "totalSteps": MessageLookupByLibrary.simpleMessage("الخطوات الكلية"), 59 | "totalStepsToday": MessageLookupByLibrary.simpleMessage("خطوات اليوم") 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /lib/generated/intl/messages_en.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that provides messages for a en locale. All the 3 | // messages from the main program should be duplicated here with the same 4 | // function name. 5 | 6 | // Ignore issues from commonly used lints in this file. 7 | // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new 8 | // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering 9 | // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases 10 | // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes 11 | // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes 12 | 13 | import 'package:intl/intl.dart'; 14 | import 'package:intl/message_lookup_by_library.dart'; 15 | 16 | final messages = new MessageLookup(); 17 | 18 | typedef String MessageIfAbsent(String messageStr, List args); 19 | 20 | class MessageLookup extends MessageLookupByLibrary { 21 | String get localeName => 'en'; 22 | 23 | final messages = _notInlinedMessages(_notInlinedMessages); 24 | static Map _notInlinedMessages(_) => { 25 | "allInOneTrack": MessageLookupByLibrary.simpleMessage( 26 | "Your All-in one Activity Tracker!"), 27 | "availableRewards": 28 | MessageLookupByLibrary.simpleMessage("Available Rewards"), 29 | "done": MessageLookupByLibrary.simpleMessage("Done"), 30 | "dummyDone": MessageLookupByLibrary.simpleMessage("Dummy Done"), 31 | "earn": MessageLookupByLibrary.simpleMessage("Earn"), 32 | "emptyState": 33 | MessageLookupByLibrary.simpleMessage("No Data Available!"), 34 | "enterYourName": 35 | MessageLookupByLibrary.simpleMessage("Enter your name"), 36 | "exchanges": MessageLookupByLibrary.simpleMessage("Exchanges"), 37 | "gainMorePoints": MessageLookupByLibrary.simpleMessage( 38 | "Congratulations! You gained more health points"), 39 | "healthPoints": MessageLookupByLibrary.simpleMessage("Health Points"), 40 | "home": MessageLookupByLibrary.simpleMessage("Home"), 41 | "leaderboard": MessageLookupByLibrary.simpleMessage("Board"), 42 | "moreComingSoon": 43 | MessageLookupByLibrary.simpleMessage("More coming soon!"), 44 | "notice": MessageLookupByLibrary.simpleMessage("Notice"), 45 | "pedometer": MessageLookupByLibrary.simpleMessage("Pedometer"), 46 | "points": MessageLookupByLibrary.simpleMessage("Pts"), 47 | "pointsLessThanItem": MessageLookupByLibrary.simpleMessage( 48 | "Your points are less than the item\'s points, walk more and try again!"), 49 | "qrCode": MessageLookupByLibrary.simpleMessage("QR Code"), 50 | "rewards": MessageLookupByLibrary.simpleMessage("Rewards"), 51 | "scanQrCode": MessageLookupByLibrary.simpleMessage( 52 | "Scan the QR Code and the points will be taken"), 53 | "somethingWentWrong": 54 | MessageLookupByLibrary.simpleMessage("Something went wrong!"), 55 | "startUsingSteps": 56 | MessageLookupByLibrary.simpleMessage("Start Using Steps"), 57 | "stepGoal": MessageLookupByLibrary.simpleMessage("Step Goal:"), 58 | "totalSteps": MessageLookupByLibrary.simpleMessage("Total Steps"), 59 | "totalStepsToday": 60 | MessageLookupByLibrary.simpleMessage("Total Steps Today") 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /lib/l10n/intl_ar.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "ar", 3 | "allInOneTrack": "كل ما تحتاجه لتتبع نشاطك!", 4 | "enterYourName": "يرجى كتابة اسمك", 5 | "startUsingSteps": "بدء الاستخدام", 6 | "somethingWentWrong": "ثمة خطأ ما!", 7 | "home": "الرئيسية", 8 | "exchanges": "تحويلات", 9 | "rewards": "جوائز", 10 | "leaderboard": "ترتيب", 11 | "emptyState": "لا يوجد بيانات متاحة!", 12 | "availableRewards": "الجوائز المتاحة", 13 | "moreComingSoon": "انتظروا المزيد قريبًا!", 14 | "pedometer": "متتبع الخطوات", 15 | "gainMorePoints": "تهانينا! لقد حصلت على المزيد من النقاط", 16 | "totalStepsToday": "خطوات اليوم", 17 | "stepGoal": "هدف اليوم", 18 | "healthPoints": "نقاط الصحة", 19 | "totalSteps": "الخطوات الكلية", 20 | "points": "نقاط", 21 | "qrCode": "رمز الاستجابة السريعة", 22 | "scanQrCode": "امسح رمز الاستجابة السريعة وسيتم سحب النقاط", 23 | "dummyDone": "شراء وهمي", 24 | "notice": "تنويه", 25 | "pointsLessThanItem": "نقاطك الحالية لا تكفي لشراء هذا المنتج، قم بالمشي أكثر وجرب مرة أخرى!", 26 | "done": "إنهاء", 27 | "earn": "احصل عليه" 28 | } -------------------------------------------------------------------------------- /lib/l10n/intl_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "en", 3 | "allInOneTrack": "Your All-in one Activity Tracker!", 4 | "enterYourName": "Enter your name", 5 | "startUsingSteps": "Start Using Steps", 6 | "somethingWentWrong": "Something went wrong!", 7 | "home": "Home", 8 | "exchanges": "Exchanges", 9 | "rewards": "Rewards", 10 | "leaderboard": "Board", 11 | "emptyState": "No Data Available!", 12 | "availableRewards": "Available Rewards", 13 | "moreComingSoon": "More coming soon!", 14 | "pedometer": "Pedometer", 15 | "gainMorePoints": "Congratulations! You gained more health points", 16 | "totalStepsToday": "Total Steps Today", 17 | "stepGoal": "Step Goal:", 18 | "healthPoints": "Health Points", 19 | "totalSteps": "Total Steps", 20 | "points": "Pts", 21 | "qrCode": "QR Code", 22 | "scanQrCode": "Scan the QR Code and the points will be taken", 23 | "dummyDone": "Dummy Done", 24 | "notice": "Notice", 25 | "pointsLessThanItem": "Your points are less than the item's points, walk more and try again!", 26 | "done": "Done", 27 | "earn": "Earn" 28 | } -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_core/firebase_core.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:flutter_localizations/flutter_localizations.dart'; 5 | import 'package:flutter_steps_tracker/di/injection_container.dart'; 6 | import 'package:flutter_steps_tracker/features/intro/presentation/manager/auth_status/auth_status_cubit.dart'; 7 | import 'package:flutter_steps_tracker/generated/l10n.dart'; 8 | import 'package:flutter_steps_tracker/utilities/locale/cubit/utility_cubit.dart'; 9 | import 'package:flutter_steps_tracker/utilities/locale/cubit/utility_state.dart'; 10 | import 'package:flutter_steps_tracker/utilities/locale/theme_data.dart'; 11 | import 'package:flutter_steps_tracker/utilities/routes/router.dart'; 12 | import 'package:flutter_steps_tracker/utilities/routes/routes.dart'; 13 | 14 | // TODO: It's not good to do many workarounds as I did in this project 15 | // but I had to do that because currently, I don't have server-side service 16 | 17 | Future main() async { 18 | await _initialize(); 19 | runApp(const MyApp()); 20 | } 21 | 22 | Future _initialize() async { 23 | WidgetsFlutterBinding.ensureInitialized(); 24 | await Firebase.initializeApp(); 25 | await configure(); 26 | } 27 | 28 | class MyApp extends StatelessWidget { 29 | const MyApp({Key? key}) : super(key: key); 30 | 31 | // This widget is the root of your application. 32 | @override 33 | Widget build(BuildContext context) { 34 | return MultiBlocProvider( 35 | providers: [ 36 | BlocProvider( 37 | lazy: false, 38 | create: (BuildContext context) { 39 | final cubit = getIt(); 40 | cubit.getCurrentTheme(); 41 | cubit.getCurrentLocale(); 42 | return cubit; 43 | }, 44 | ), 45 | BlocProvider( 46 | lazy: false, 47 | create: (context) { 48 | final cubit = getIt(); 49 | cubit.checkAuthStatus(); 50 | return cubit; 51 | }, 52 | ), 53 | ], 54 | child: GestureDetector( 55 | onTap: () { 56 | WidgetsBinding.instance.focusManager.primaryFocus?.unfocus(); 57 | }, 58 | child: Builder(builder: (context) { 59 | final utility = BlocProvider.of(context); 60 | return BlocBuilder( 61 | bloc: utility, 62 | builder: (context, state) { 63 | return MaterialApp( 64 | debugShowCheckedModeBanner: false, 65 | title: 'Flutter Steps Tracker', 66 | theme: MainTheme.lightTheme(context), 67 | supportedLocales: S.delegate.supportedLocales, 68 | localizationsDelegates: const [ 69 | S.delegate, 70 | GlobalMaterialLocalizations.delegate, 71 | GlobalWidgetsLocalizations.delegate, 72 | GlobalCupertinoLocalizations.delegate, 73 | ], 74 | locale: utility.locale, 75 | themeMode: utility.currentTheme(), 76 | darkTheme: MainTheme.darkTheme(context), 77 | onGenerateRoute: onGenerate, 78 | initialRoute: AppRoutes.landingPageRoute, 79 | ); 80 | }, 81 | ); 82 | }), 83 | ), 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/utilities/constants/api_path.dart: -------------------------------------------------------------------------------- 1 | class APIPath { 2 | static String user(String uid) => 'users/$uid'; 3 | static String users() => 'users/'; 4 | 5 | static String setDailyStepsAndPoints(String uid, String id) => 6 | 'users/$uid/dailyPoints/$id'; 7 | 8 | static String setMyReward(String uid, String id) => 'users/$uid/rewards/$id'; 9 | 10 | static String myRewards(String uid) => 'users/$uid/rewards/'; 11 | 12 | static String dailyStepsAndPointsStream(String uid) => 13 | 'users/$uid/dailyPoints/'; 14 | 15 | static String rewards() => 'rewards/'; 16 | 17 | static String exchangeHistory(String uid, String exchangeId) => 18 | 'users/$uid/exchanges/$exchangeId'; 19 | 20 | static String exchangesHistory(String uid) => 'users/$uid/exchanges/'; 21 | } 22 | -------------------------------------------------------------------------------- /lib/utilities/constants/app_colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppColors { 4 | static const kBlackColor = Color(0xFF000000); 5 | static const kScaffoldBackgroundColor = Color(0xFFFFFFFF); 6 | static const kPrimaryColor = Colors.blue; 7 | static const kErrorColor = Colors.red; 8 | static const kRedAccentColor = Colors.redAccent; 9 | static const kGreyColor = Colors.grey; 10 | } 11 | -------------------------------------------------------------------------------- /lib/utilities/constants/assets.dart: -------------------------------------------------------------------------------- 1 | class AppAssets { 2 | /// External (Network) 3 | static const String logo = 4 | 'https://iconape.com/wp-content/files/yn/145105/png/145105.png'; 5 | static const String dummyUserImage = 6 | 'https://www.business2community.com/wp-content/plugins/wp-user-avatars/wp-user-avatars/assets/images/mystery.jpg'; 7 | static const String strongArm = 8 | 'https://cdn.emojidex.com/emoji/seal/muscle.png'; 9 | static const String exchangesIcon = 'https://g.top4top.io/p_2359tsmp22.png'; 10 | static const String rewardsIcon = 'https://f.top4top.io/p_23591c6wg1.png'; 11 | 12 | /// Internal (Assets) 13 | static const String manInBackgroundIntro = 'assets/images/man-intro.jpeg'; 14 | static const String rewardsBanner = 'assets/images/rewards.png'; 15 | } 16 | -------------------------------------------------------------------------------- /lib/utilities/constants/enums.dart: -------------------------------------------------------------------------------- 1 | enum AuthStatus { authenticated, unAuthenticated } 2 | 3 | enum ExchangeHistoryTitle { 4 | exchange( 5 | 'Congratulations, you gained more 5 health points because of your footsteps!'), 6 | reward('were taken from your health points because of the reward!'); 7 | 8 | final String title; 9 | 10 | const ExchangeHistoryTitle(this.title); 11 | } 12 | -------------------------------------------------------------------------------- /lib/utilities/constants/key_constants.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | class KeyConstants { 4 | /// Locale details 5 | static const englishLocale = Locale('en', 'US'); 6 | static const arabicLocale = Locale('ar', 'EG'); 7 | 8 | /// Shared Preferences 9 | static const String currentUser = "CURRENT_USER"; 10 | static const String localeKey = "LOCALE"; 11 | static const String theme = "THEME"; 12 | static const String isAppLocaleSet = 'IS_APP_LOCALE_SET'; 13 | } 14 | -------------------------------------------------------------------------------- /lib/utilities/locale/cubit/utility_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_steps_tracker/core/data/data_sources/cache_helper.dart'; 4 | import 'package:flutter_steps_tracker/di/injection_container.dart'; 5 | import 'package:flutter_steps_tracker/utilities/constants/key_constants.dart'; 6 | import 'package:flutter_steps_tracker/utilities/locale/cubit/utility_state.dart'; 7 | import 'package:injectable/injectable.dart'; 8 | 9 | @injectable 10 | class UtilityCubit extends Cubit { 11 | UtilityCubit() : super(const UtilityState.initial()); 12 | 13 | Locale locale = KeyConstants.englishLocale; 14 | bool isDark = false; 15 | 16 | ThemeMode currentTheme() => isDark ? ThemeMode.dark : ThemeMode.light; 17 | 18 | Future getCurrentTheme() async { 19 | await getIt().has(KeyConstants.theme).then((hasToken) async { 20 | debugPrint('Has Token: $hasToken'); 21 | if (hasToken) { 22 | await getIt().get(KeyConstants.theme).then((value) async { 23 | debugPrint('Value isDark: $value'); 24 | isDark = value as bool; 25 | }); 26 | } else { 27 | isDark = false; 28 | } 29 | }); 30 | emit(const UtilityState.changeState()); 31 | } 32 | 33 | Future switchTheme() async { 34 | isDark = !isDark; 35 | await getIt().put(KeyConstants.theme, isDark); 36 | emit(UtilityState.reloadingTheme(isDark)); 37 | } 38 | 39 | Future getCurrentLocale() async { 40 | await getIt() 41 | .has(KeyConstants.localeKey) 42 | .then((hasToken) async { 43 | if (hasToken) { 44 | await getIt() 45 | .get(KeyConstants.localeKey) 46 | .then((value) async { 47 | locale = Locale.fromSubtags( 48 | languageCode: value.split('_').first, 49 | countryCode: value.split('_').last); 50 | }); 51 | } else { 52 | locale = KeyConstants.englishLocale; 53 | } 54 | }); 55 | emit(const UtilityState.changeState()); 56 | } 57 | 58 | Future changeLocale(Locale lc) async { 59 | locale = lc; 60 | await getIt().put(KeyConstants.localeKey, locale.toString()); 61 | debugPrint('changedLocale'); 62 | emit(UtilityState.reloadingLocale(locale)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/utilities/locale/cubit/utility_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'utility_state.freezed.dart'; 5 | 6 | @freezed 7 | class UtilityState with _$UtilityState { 8 | const factory UtilityState.initial() = Initial; 9 | const factory UtilityState.loading() = Loading; 10 | const factory UtilityState.reloadingTheme(bool isDark) = ReloadingTheme; 11 | const factory UtilityState.reloadingLocale(Locale locale) = ReloadingLocale; 12 | const factory UtilityState.changeState() = ChangeState; 13 | const factory UtilityState.error({required String message}) = Error; 14 | } 15 | -------------------------------------------------------------------------------- /lib/utilities/locale/theme_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_steps_tracker/utilities/constants/app_colors.dart'; 3 | 4 | class MainTheme { 5 | static ThemeData lightTheme(BuildContext context) { 6 | return ThemeData( 7 | primaryColor: AppColors.kPrimaryColor, 8 | scaffoldBackgroundColor: AppColors.kScaffoldBackgroundColor, 9 | backgroundColor: AppColors.kRedAccentColor, 10 | primaryColorDark: AppColors.kGreyColor, 11 | primaryColorLight: AppColors.kPrimaryColor, 12 | dividerTheme: DividerThemeData( 13 | color: AppColors.kGreyColor.withOpacity(0.4), 14 | ), 15 | iconTheme: const IconThemeData( 16 | color: AppColors.kScaffoldBackgroundColor, 17 | ), 18 | appBarTheme: const AppBarTheme( 19 | elevation: 0, 20 | backgroundColor: AppColors.kScaffoldBackgroundColor, 21 | ), 22 | inputDecorationTheme: InputDecorationTheme( 23 | fillColor: AppColors.kScaffoldBackgroundColor, 24 | filled: true, 25 | border: OutlineInputBorder( 26 | borderRadius: BorderRadius.circular(24.0), 27 | borderSide: const BorderSide( 28 | color: AppColors.kPrimaryColor, 29 | ), 30 | ), 31 | focusedBorder: OutlineInputBorder( 32 | borderRadius: BorderRadius.circular(24.0), 33 | borderSide: const BorderSide( 34 | color: AppColors.kPrimaryColor, 35 | ), 36 | ), 37 | errorBorder: OutlineInputBorder( 38 | borderRadius: BorderRadius.circular(24.0), 39 | borderSide: const BorderSide( 40 | color: AppColors.kErrorColor, 41 | ), 42 | ), 43 | focusedErrorBorder: OutlineInputBorder( 44 | borderRadius: BorderRadius.circular(24.0), 45 | borderSide: const BorderSide( 46 | color: AppColors.kErrorColor, 47 | ), 48 | ), 49 | disabledBorder: OutlineInputBorder( 50 | borderRadius: BorderRadius.circular(24.0), 51 | borderSide: const BorderSide( 52 | color: AppColors.kPrimaryColor, 53 | ), 54 | ), 55 | enabledBorder: OutlineInputBorder( 56 | borderRadius: BorderRadius.circular(24.0), 57 | borderSide: const BorderSide( 58 | color: AppColors.kPrimaryColor, 59 | ), 60 | ), 61 | ), 62 | canvasColor: Colors.transparent, 63 | errorColor: AppColors.kErrorColor, 64 | hoverColor: Colors.transparent, 65 | highlightColor: Colors.transparent, 66 | splashColor: Colors.transparent, 67 | ); 68 | } 69 | 70 | static ThemeData darkTheme(BuildContext context) { 71 | return ThemeData( 72 | primaryColor: AppColors.kScaffoldBackgroundColor, 73 | scaffoldBackgroundColor: AppColors.kBlackColor, 74 | backgroundColor: AppColors.kRedAccentColor, 75 | primaryColorLight: AppColors.kPrimaryColor, 76 | primaryColorDark: AppColors.kScaffoldBackgroundColor, 77 | dividerTheme: DividerThemeData( 78 | color: AppColors.kScaffoldBackgroundColor.withOpacity(0.4), 79 | ), 80 | iconTheme: const IconThemeData( 81 | color: AppColors.kBlackColor, 82 | ), 83 | listTileTheme: const ListTileThemeData( 84 | textColor: AppColors.kScaffoldBackgroundColor, 85 | ), 86 | cardColor: Colors.grey, 87 | appBarTheme: const AppBarTheme( 88 | elevation: 0, 89 | backgroundColor: AppColors.kScaffoldBackgroundColor, 90 | ), 91 | inputDecorationTheme: InputDecorationTheme( 92 | fillColor: AppColors.kScaffoldBackgroundColor, 93 | filled: true, 94 | border: OutlineInputBorder( 95 | borderRadius: BorderRadius.circular(24.0), 96 | borderSide: const BorderSide( 97 | color: AppColors.kPrimaryColor, 98 | ), 99 | ), 100 | focusedBorder: OutlineInputBorder( 101 | borderRadius: BorderRadius.circular(24.0), 102 | borderSide: const BorderSide( 103 | color: AppColors.kPrimaryColor, 104 | ), 105 | ), 106 | errorBorder: OutlineInputBorder( 107 | borderRadius: BorderRadius.circular(24.0), 108 | borderSide: const BorderSide( 109 | color: AppColors.kErrorColor, 110 | ), 111 | ), 112 | focusedErrorBorder: OutlineInputBorder( 113 | borderRadius: BorderRadius.circular(24.0), 114 | borderSide: const BorderSide( 115 | color: AppColors.kErrorColor, 116 | ), 117 | ), 118 | disabledBorder: OutlineInputBorder( 119 | borderRadius: BorderRadius.circular(24.0), 120 | borderSide: const BorderSide( 121 | color: AppColors.kPrimaryColor, 122 | ), 123 | ), 124 | enabledBorder: OutlineInputBorder( 125 | borderRadius: BorderRadius.circular(24.0), 126 | borderSide: const BorderSide( 127 | color: AppColors.kPrimaryColor, 128 | ), 129 | ), 130 | ), 131 | canvasColor: Colors.transparent, 132 | errorColor: AppColors.kErrorColor, 133 | hoverColor: Colors.transparent, 134 | highlightColor: Colors.transparent, 135 | splashColor: Colors.transparent, 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/utilities/routes/router.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter_steps_tracker/core/presentation/pages/landing_page.dart'; 3 | import 'package:flutter_steps_tracker/features/bottom_navbar/presentation/pages/bottom_navbar.dart'; 4 | import 'package:flutter_steps_tracker/utilities/routes/routes.dart'; 5 | 6 | Route onGenerate(RouteSettings settings) { 7 | switch (settings.name) { 8 | case AppRoutes.homePageRoute: 9 | return CupertinoPageRoute( 10 | builder: (_) => const BottomNavbar(), 11 | settings: settings, 12 | ); 13 | case AppRoutes.landingPageRoute: 14 | default: 15 | return CupertinoPageRoute( 16 | builder: (_) => const LandingPage(), 17 | settings: settings, 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/utilities/routes/routes.dart: -------------------------------------------------------------------------------- 1 | class AppRoutes { 2 | static const String landingPageRoute = '/'; 3 | static const String homePageRoute = '/bottom_navbar'; 4 | } 5 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_steps_tracker 2 | description: A new Flutter project. 3 | 4 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 5 | version: 1.0.0+1 6 | 7 | environment: 8 | sdk: ">=2.17.3 <3.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | cupertino_icons: ^1.0.2 14 | flutter_localizations: 15 | sdk: flutter 16 | pedometer: ^3.0.0 17 | firebase_core: ^1.17.1 18 | firebase_auth: ^3.3.19 19 | cloud_firestore: ^3.1.17 20 | get_it: ^7.2.0 21 | injectable: ^1.5.3 22 | freezed: ^2.0.3+1 23 | freezed_annotation: ^2.0.3 24 | provider: ^6.0.3 25 | flutter_bloc: ^8.0.1 26 | equatable: ^2.0.3 27 | shared_preferences: ^2.0.15 28 | persistent_bottom_nav_bar_v2: ^4.2.3 29 | syncfusion_flutter_gauges: ^20.1.59 30 | dartz: ^0.10.1 31 | logging: ^1.0.2 32 | intl: ^0.17.0 33 | cached_network_image: ^3.2.1 34 | cron: ^0.5.0 35 | 36 | dev_dependencies: 37 | flutter_test: 38 | sdk: flutter 39 | flutter_lints: ^2.0.0 40 | build_runner: 41 | injectable_generator: ^1.5.3 42 | change_app_package_name: ^1.1.0 43 | 44 | flutter_intl: 45 | enabled: true 46 | 47 | flutter: 48 | uses-material-design: true 49 | 50 | assets: 51 | - assets/images/ 52 | # - images/a_dot_ham.jpeg -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:flutter_steps_tracker/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | --------------------------------------------------------------------------------